---
title: "Python SDK · API docs · CheckAnySite"
description: "The official Python library: typed responses, sync and async clients, and built-in retries."
url: "https://docs.checkanysite.com/sdks/python"
---

Get started

# Python SDK

The official Python library: typed responses, sync and async clients, and built-in retries.

## Install

The library supports Python 3.9 and later. Install it from PyPI:

```bash Terminal
pip install checkanysite
```

## Quickstart

`CheckAnySite()` reads your key from the `CHECKANYSITE_API_KEY` environment variable; pass `api_key=` to use another. The following example checks whether a site shows traces of an AI website builder:

```python
from checkanysite import CheckAnySite

client = CheckAnySite()  # Reads the key from CHECKANYSITE_API_KEY.

site = client.sites.retrieve("stripe.com", checks=["ai"])

if site.ai.level == "none":
    print(f"No AI builder traces found on {site.domain}.")
else:
    builders = ", ".join(builder.name for builder in site.ai.builders)
    print(f"{site.domain}: traces of {builders}, {site.ai.level} confidence")

print(f"Evidence: {site.result_url}")
```

`client.sites.retrieve` returns our stored result for a site straight away and never starts a scan. For a site we haven’t checked yet, it raises `NotFoundError`.

## Scans

`client.scans.create_and_wait` starts a scan and polls it until it is complete; most scans take a few seconds. To start a scan without waiting, call `client.scans.create`, then `client.scans.wait` when you need the result. A stored result costs fewer credits than a new scan, so the following function uses one when it exists and scans the site otherwise:

```python
from checkanysite import CheckAnySite, NotFoundError

client = CheckAnySite()

def ai_traces(domain):
    """Returns the AI builder traces of a site, or None if its scan failed."""
    try:
        return client.sites.retrieve(domain, checks=["ai"]).ai
    except NotFoundError:
        scan = client.scans.create_and_wait(domain, checks=["ai"])
        return scan.result.ai if scan.status == "completed" else None
```

If a scan takes longer than `timeout` (120 seconds by default), the library raises `ScanTimeoutError`. The scan keeps running, so you can read it later with `client.scans.retrieve(error.scan_id)`. To check many sites in one request, see [Batches](https://docs.checkanysite.com/batches).

## Errors

When the API returns an error, the library raises a subclass of `checkanysite.APIStatusError`: `AuthenticationError` for `401`, `NotFoundError` for `404`, `RateLimitError` for `429`, and so on. Each carries the error’s `code` and `message`, and the `request_id` to include when you contact us. When the API can’t be reached, it raises `checkanysite.APIConnectionError`. The [errors](https://docs.checkanysite.com/errors) page explains every code.

```python
import logging

import checkanysite
from checkanysite import CheckAnySite

logger = logging.getLogger(__name__)
client = CheckAnySite()

try:
    site = client.sites.retrieve("wikipedia.org", checks=["ai"])
except checkanysite.AuthenticationError as error:
    logger.error("CheckAnySite rejected the API key: %s", error.message)
    raise
except checkanysite.RateLimitError as error:
    # Requests over the rate limit were already retried. A spent monthly or
    # daily limit lasts until its reset, which the message gives.
    logger.warning("CheckAnySite limit reached: %s", error.message)
    raise
except checkanysite.APIStatusError as error:
    logger.error(
        "CheckAnySite returned %s (%s): %s Request ID: %s",
        error.status_code,
        error.code,
        error.message,
        error.request_id,
    )
    raise
```

## Retries

- The library retries a failed request twice by default: connection errors and timeouts, `409`, `429 rate_limited`, and statuses of 500 and above. It waits as long as the `Retry-After` header asks, or backs off exponentially. Set `max_retries` to change this.
- It never retries a spent limit: `quota_exceeded`, `key_limit_reached` or `scan_limit_reached`. Those last until their reset.
- Every POST request carries an `Idempotency-Key` header, the same on each retry, so a retried scan is never started or charged twice ([Idempotency](https://docs.checkanysite.com/idempotency)).

## Async

`AsyncCheckAnySite` has the same methods as `CheckAnySite`; await each call. Open it with `async with` to close its connections when you’re done. The following example finds the framework of two sites at once:

```python
import asyncio

from checkanysite import AsyncCheckAnySite

async def main():
    async with AsyncCheckAnySite() as client:
        sites = await asyncio.gather(
            client.sites.retrieve("stripe.com", checks=["tech"]),
            client.sites.retrieve("vercel.com", checks=["tech"]),
        )
    for site in sites:
        frameworks = [
            item.name
            for item in site.tech.technologies
            if item.category == "Framework"
        ]
        print(f"{site.domain}: {', '.join(frameworks) or 'no framework found'}")

asyncio.run(main())
```
