Skip to main content

CheckAnySite Python library

The official Python library for the CheckAnySite API: what is this website? Ask about any public site and get, with evidence, the traces of AI website builders, its AI readiness, popularity, technology, DNS records, trust signals, a summary and its history.

It gives typed answers (pydantic models), a sync and an async client, retries with backoff, idempotent POSTs and the credits each call cost. Python 3.9 or newer; its only dependencies are httpx and pydantic.

Install

pip install checkanysite

Quickstart

Get a key free at checkanysite.com/account/keys and put it in the environment:

export CHECKANYSITE_API_KEY="cas_live_…"
from checkanysite import CheckAnySite

client = CheckAnySite()  # reads CHECKANYSITE_API_KEY

site = client.sites.retrieve("stripe.com", checks=["ai", "tech"], max_age_days=30)
print(site.ai.level)  # "high", "medium" or "none"
for technology in site.tech.technologies:
    print(technology.name, technology.category)

print(site.meta.credits_cost, site.meta.credits_remaining, site.meta.request_id)

There are two APIs behind one key:

  • Lookup (client.sites) answers from stored results at once and never scans. A found result costs each check asked at its stored price; a miss (no_result) is free.
  • Scan (client.scans) scans a site live. A scan answers asynchronously: start it, then poll it, or let create_and_wait poll for you.
scan = client.scans.create("example.com", checks=["readiness"])  # a Scan, maybe still queued
scan = client.scans.retrieve(scan.id)  # read it again, free
scan = client.scans.wait(scan)  # poll it until it is complete, free

scan = client.scans.create_and_wait("example.com", checks=["readiness"], timeout=120)
if scan.status == "completed":
    print(scan.result.readiness.score)
else:
    print(scan.failure.code, scan.failure.message)

usage = client.usage.retrieve()
print(usage.remaining, "of", usage.allowance, "credits left this month")

The checks are ai, readiness, popularity, tech, dns, trust, summary and history; leave checks out to ask every check. A scan runs only what its checks need, so fewer checks are faster and cost less.

wait and create_and_wait wait as long as each answer's Retry-After asks (3 seconds today) and return the scan once it is complete: completed with every slow source in, or failed. When their timeout (120 seconds by default) comes first, they raise ScanTimeoutError, which names the scan's id and holds the scan as last read; the scan goes on, so read it later with client.scans.retrieve(error.scan_id) or wait again with client.scans.wait(error.scan_id).

Batches

batch = client.sites.batch(["stripe.com", "lovable.dev"], checks=["ai"])
for item in batch.data:  # one item per unique domain, in the order first sent
    if item.result:
        print(item.domain, item.result.ai.level)
    else:
        print(item.input, item.error.code)  # no_result, invalid_domain, …

from checkanysite.models import Scan

scans = client.scans.batch_and_wait(["stripe.com", "lovable.dev"], checks=["ai"])
for item in scans.data:
    if not isinstance(item, Scan):  # a ScanBatchFailure: this one couldn't be scanned
        print(item.input, item.error.code)
    elif item.status == "completed":
        print(item.domain, item.result.ai.level)
    else:  # failed
        print(item.domain, item.failure.code)

A lookup batch takes 1 to 50 domains or URLs, a scan batch 1 to 10. scans.batch starts the scans and answers at once; batch_and_wait also waits until every one is complete, within one timeout for them all, and keeps the batch's meta (what it cost).

What each answer's headers said

Every object a method returns has .meta, read from the answer's headers:

Attribute Header
request_id X-Request-Id: quote it when you write us
credits_cost, credits_remaining, credits_reset X-Credits-* (on calls that cost credits)
rate_limit_limit, rate_limit_remaining, rate_limit_reset X-RateLimit-*
retry_after_seconds Retry-After
idempotent_replayed Idempotent-Replayed
status_code, etag, headers the status, ETag, every header

Errors

Every error is an APIError. When the API answers with an error, the class comes from the status, and .code says exactly what happened:

import checkanysite

domain = "example.com"
try:
    site = client.sites.retrieve(domain, checks=["ai"])
except checkanysite.NotFoundError as error:
    if error.code == "no_result":  # nothing stored for this site yet: scan it
        scan = client.scans.create_and_wait(domain, checks=["ai"])
except checkanysite.RateLimitError as error:
    print(error.code)  # rate_limited, or a limit used up, such as quota_exceeded
except checkanysite.APIStatusError as error:
    print(error.status_code, error.code, error.message, error.param, error.request_id, error.docs)
except checkanysite.APIConnectionError:
    print("No answer from the API")
Status Class Codes
400 BadRequestError invalid_json, invalid_parameter, invalid_domain
401 AuthenticationError missing_api_key, invalid_api_key, revoked_api_key, expired_api_key
403 PermissionDeniedError read_only_key, browser_request
404 NotFoundError not_found, no_result, scan_not_found
409 ConflictError idempotency_key_in_use
422 UnprocessableEntityError site_refused, idempotency_key_reused
429 RateLimitError rate_limited, quota_exceeded, key_limit_reached, scan_limit_reached
500 and up InternalServerError internal_error, capacity
other APIStatusError method_not_allowed (405), request_too_large (413), …
no answer APIConnectionError APITimeoutError when the request timed out

New codes may appear within v1; they land in the class for their status. Every code is explained on the errors page.

Retries

A failure that may pass is retried twice by default, with exponential backoff (0.5, 1, 2 … up to 8 seconds) and jitter, waiting as long as Retry-After asks when the API sends it:

  • no answer (a connection error or a timeout);
  • 409 idempotency_key_in_use;
  • 429 rate_limited, but never quota_exceeded or key_limit_reached (the month's credits are spent) or scan_limit_reached (the day's new scans are started);
  • 500, 502, 503 and 504.

A Retry-After longer than 60 seconds isn't waited out: the error is raised so you can decide.

client = CheckAnySite(max_retries=5)  # or 0 to turn retrying off

Idempotency

Every POST carries an Idempotency-Key, a new UUID per call, kept across that call's retries: if an answer is lost and the call is retried, the API replays the first answer instead of scanning (and charging) twice. Pass your own key to make a call safe to repeat across runs:

scan = client.scans.create("example.com", idempotency_key="order-1234")
scan.meta.idempotent_replayed  # True when this is the stored first answer

A key is kept 24 hours. The same key with a different body raises UnprocessableEntityError (idempotency_key_reused).

Timeouts and configuration

import httpx
from checkanysite import CheckAnySite

client = CheckAnySite(
    api_key="cas_live_…",  # default: CHECKANYSITE_API_KEY
    base_url="https://api.checkanysite.com/v1",  # default: CHECKANYSITE_BASE_URL, else this
    timeout=30.0,  # seconds per request, or an httpx.Timeout
    max_retries=2,
    http_client=httpx.Client(proxy="http://proxy.internal:8080"),  # optional; you close it
)

Use the client as a context manager (with CheckAnySite() as client:) or call client.close() when done.

Async

AsyncCheckAnySite has the same methods, awaited:

import asyncio
from checkanysite import AsyncCheckAnySite


async def main() -> None:
    async with AsyncCheckAnySite() as client:
        sites = await asyncio.gather(
            client.sites.retrieve("stripe.com", checks=["ai"]),
            client.sites.retrieve("lovable.dev", checks=["ai"]),
        )
        scan = await client.scans.create_and_wait("example.com", checks=["dns"])


asyncio.run(main())

Models

The answers are pydantic models generated from the API's OpenAPI document, in checkanysite.models. Attributes are snake_case (site.scanned_at); model_dump(by_alias=True) gives the API's camelCase JSON. Within v1 the API may add fields and values, never remove or rename them, so the models keep what they don't know: new fields are in model.model_extra, and new values of a field such as level arrive as strings.

Versioning

This library follows semantic versioning and the API v1. Changes are listed in CHANGELOG.md.

Development

From sdks/python/, with uv:

uv run ruff format --check . && uv run ruff check . && uv run mypy && uv run pytest
uv build                                   # an sdist and a wheel in dist/

The models come from the server's schemas. After an API change, from the repository root:

pnpm sdk:python:models  # exports openapi.json, then regenerates src/checkanysite/models/_generated.py

Metadata

Release files for checkanysite 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for checkanysite 0.1.0
File Size Uploaded Provenance Actions
checkanysite-0.1.0.tar.gz 56.5 kB Without provenance Details for checkanysite-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for checkanysite 0.1.0
File Interpreter ABI Platform Actions
checkanysite-0.1.0-py3-none-any.whl Python 3 none any Details for checkanysite-0.1.0-py3-none-any.whl

Total release size: 92.3 kB

Release files / checkanysite-0.1.0.tar.gz

Download URL checkanysite-0.1.0.tar.gz
Size 56.5 kB
Tags Source
SHA-256 checksum
How to use checksums
eae80f3253bcfda253057c67ae3330cfd97cb7281cfa775cb9af1c085cdd2879
BLAKE2b-256 checksum
How to use checksums
6e8f3a4b5cf350a741760cea6e3fc9275edf6e306ac63eabd8387b60cd0d175c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.13

Release files / checkanysite-0.1.0-py3-none-any.whl

Download URL checkanysite-0.1.0-py3-none-any.whl
Size 35.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fccd6e279c017d3f2a3f4b460e24616b6028ee545d4ea06cca57e9afbad483be
BLAKE2b-256 checksum
How to use checksums
dc5d2176b24b5c8c087d9ced64fb6c1628c02642bb562097be3e4ea6f25c16bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.13

Security for checkanysite 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page