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 letcreate_and_waitpoll 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 neverquota_exceededorkey_limit_reached(the month's credits are spent) orscan_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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.