# CheckAnySite API > Find out what any website is, from your code or your AI agent: traces of AI website builders, AI readiness, popularity, the technology it runs on, DNS records, trust signals, what it is and its domain history. Every finding comes with its evidence and source. - Base URL: `https://api.checkanysite.com/v1` (also at `https://checkanysite.com/api/v1`) - OpenAPI: https://api.checkanysite.com/v1/openapi.json - MCP server: https://checkanysite.com/mcp - API keys: https://checkanysite.com/account/keys (free during launch: 300 credits a month, no card required, more on request) - Docs: https://docs.checkanysite.com · Status: https://status.checkanysite.com Field names are camelCase, and times are ISO 8601 in UTC. ## Two APIs, one key - **Lookup API**: our stored results, returned straight away, never a scan. `GET /sites/{domain}` returns a site's latest stored result, or 404 `no_result` when there is none. `POST /sites/batch` looks up as many as 50 sites. - **Scan API**: a live scan. `POST /scans` returns 202 with the scan (its `Location` and `Retry-After` headers say where and when to read it), or 200 when a result from the last 24 hours has the checks. `GET /scans/{id}` reads a scan for free, and `POST /scans/batch` scans as many as 10 sites. ## Authentication Send your key as a bearer token in the `Authorization` header (`Bearer cas_live_…`), or in an `X-API-Key` header. Keep it in an environment variable such as `CHECKANYSITE_API_KEY`, never in code or in a browser. The API is for servers: it sends no CORS headers and answers a browser's preflight request with 403 `browser_request`. A 401 response includes `WWW-Authenticate: Bearer realm="checkanysite"`. Only the MCP server accepts a key in its URL (`?key=`), for clients that can't send headers; keep that URL as private as the key. An account can hold up to 5 working keys; each can have its own monthly limit and expiry, and can be revoked at any time. A key is `cas_live_` followed by 32 letters and digits and a 6-character checksum (CRC32), so secret scanners can recognise a leaked key. Keys created before the checksum keep working. ## Keys and security - **Access.** A key with full access reads stored results and starts scans. A read-only key reads stored results, scans and usage, but can't start a scan (`read_only_key`), so a request with it costs at most a stored result's credits. - **Limits.** You can give a key its own monthly credit limit (`key_limit_reached`). All of an account's keys share its 300 monthly credits. - **Expiry.** A key either never expires or stops working 30, 90 or 365 days after you create it. We email you 7 days before it expires. - **Usage emails.** We email you when your monthly credits pass 80% and 100%. You can turn these emails off under Usage in your account. ### Rotating a key Rotating a key replaces it without downtime. On your API keys page, you get a new key with the same name, access and limit (and the same lifetime, if the old key expires), shown once. The old key keeps working for the grace period you choose (an hour, 24 hours or a week), then stops. 1. Rotate the key with a grace period long enough to deploy the new key; a day suits most apps. 2. Replace the old key everywhere it's used: your secrets manager, environment or agent settings. 3. When the old key's "last used" time stops changing, revoke it on your API keys page, or let its grace period end. ### If a key leaks Rotate the key with "At once", which stops the old key immediately, or revoke it. Then review the latest requests under Usage in your account: each shows when it was made, what it asked for and its request ID. We store only a SHA-256 hash of each key, never the key itself. ## Quickstart ```bash export CHECKANYSITE_API_KEY="cas_live_…" # Two checks from a stored result (2 credits). curl "https://api.checkanysite.com/v1/sites/wikipedia.org?checks=ai,dns" \ -H "Authorization: Bearer $CHECKANYSITE_API_KEY" # A new scan for one check (3 credits). # Read its links.self until "complete" is true. curl -X POST "https://api.checkanysite.com/v1/scans" \ -H "Authorization: Bearer $CHECKANYSITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "wikipedia.org", "checks": ["ai"]}' ``` The same with the Python SDK (`pip install checkanysite`): ```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}") ``` ## Endpoints ### GET /sites/{domain} Retrieve a site’s stored result. Returns our latest stored result for a site, with the checks you ask for (every check if you leave out `checks`); the other fields are null. This is the Lookup API: it never starts a scan. Check `scannedAt`, use `maxAgeDays` to require a more recent result, and scan the site with POST /scans when you need a new one. Every response has an `ETag`: send it back in `If-None-Match`, and while the result is unchanged you get a free 304 with no body. Credits: Each check you ask for costs its stored price, up to 3 for every check, when a result is found. A no_result error or a 304 costs nothing. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `domain` | path | yes | The site’s domain, such as `example.com`. | | `checks` | query | no | The checks to answer, separated by commas. Leave it out to ask for every check. Any of, separated by commas: `ai`, `readiness`, `popularity`, `tech`, `dns`, `trust`, `summary`, `history`. | | `maxAgeDays` | query | no | Return a result only if it is at most this many days old (1 to 3650); otherwise the API returns `no_result`. | | `If-None-Match` | header | no | The `ETag` of a response you already have. While the result is unchanged, the API returns a 304 with no body, for free. | Answers: 200 `SiteResult` (The stored result, with its `ETag`.); 304 (The result hasn’t changed since the `ETag` you sent. No body, and no credits.) Errors: `invalid_domain`, `invalid_parameter`, `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `rate_limited`, `no_result`, `quota_exceeded`, `key_limit_reached`, `internal_error` ### POST /sites/batch Retrieve stored results for many sites. Looks up as many as 50 sites at once, and returns each site’s stored result as GET /sites/{domain} does, in the order you sent them. A site sent twice gets one item. A site without a stored result, or an input that isn’t a domain, gets an `error` in its place, and the other sites are still returned. It never starts a scan. Credits: Each result found costs its checks’ stored price, up to 3 each; sites without a result cost nothing. The batch is charged as a whole: if your credits don’t cover every result found, the request is refused and nothing is charged. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | A unique value for this request, such as a UUID. A retry with the same key within 24 hours returns the first response (`Idempotent-Replayed: true`), and never starts a second scan or charges twice. The same key with a different request returns `idempotency_key_reused`. | Body: ```json { "domains": [ "stripe.com", "lovable.dev" ], "checks": [ "ai", "tech" ] } ``` Answers: 200 `SiteBatch` (A result or an error for each site.) Errors: `invalid_json`, `invalid_parameter`, `unsupported_media_type`, `request_too_large`, `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `rate_limited`, `idempotency_key_reused`, `idempotency_key_in_use`, `quota_exceeded`, `key_limit_reached`, `internal_error` ### POST /scans Scan a site. Starts a scan of a site with the checks you ask for. If a result from the last day already has those checks, the API returns it with 200. If a scan of the site is already running, it returns that scan with 202; otherwise it starts a new scan and returns 202. Read the scan at `links.self` until `complete` is true: the first results arrive within seconds, and slower sources follow. A scan runs only what its checks need, so fewer checks are faster and cost less. A read-only key gets a recent result or a running scan, but never starts a new one (`read_only_key`). To check a fix, ask for the `readiness` check with `again: true`: once its last result is a few minutes old, the check runs again. Credits: Each check costs its scan price, up to 15 for every check, when a new scan starts; its stored price, up to 3, when a recent result or a running scan answers. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | A unique value for this request, such as a UUID. A retry with the same key within 24 hours returns the first response (`Idempotent-Replayed: true`), and never starts a second scan or charges twice. The same key with a different request returns `idempotency_key_reused`. | Body: ```json { "domain": "example.com", "checks": [ "ai", "dns" ] } ``` Answers: 200 `Scan` (A recent result had the checks; the scan is complete.); 202 `Scan` (A scan is running. Read it at `links.self`, which the `Location` header also gives.) Errors: `invalid_json`, `invalid_parameter`, `invalid_domain`, `unsupported_media_type`, `request_too_large`, `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `rate_limited`, `idempotency_key_reused`, `idempotency_key_in_use`, `read_only_key`, `site_refused`, `quota_exceeded`, `key_limit_reached`, `scan_limit_reached`, `capacity`, `internal_error` ### POST /scans/batch Scan many sites. Scans as many as 10 sites at once. Each site is handled as POST /scans would handle it (a recent result, a running scan or a new scan), in the order you sent them. A site refused by your credits, the daily limits or our rules gets an `error` in its place, and the other sites go ahead. Read each scan at its `links.self`. Credits: Each site is charged as POST /scans charges it: its checks’ scan price for a new scan, or their stored price for a recent result or a running scan. Each scan’s credits are counted before the next site. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | A unique value for this request, such as a UUID. A retry with the same key within 24 hours returns the first response (`Idempotent-Replayed: true`), and never starts a second scan or charges twice. The same key with a different request returns `idempotency_key_reused`. | Body: ```json { "domains": [ "example.com", "example.org" ], "checks": [ "ai" ] } ``` Answers: 200 `ScanBatch` (Every scan is complete, from recent results.); 202 `ScanBatch` (Some scans are running. Read each at its `links.self`.) Errors: `invalid_json`, `invalid_parameter`, `unsupported_media_type`, `request_too_large`, `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `rate_limited`, `idempotency_key_reused`, `idempotency_key_in_use`, `internal_error` ### GET /scans/{id} Retrieve a scan. Returns a scan’s status and, once it has completed, its result with the checks that were asked for. Read the scan at its `links.self`, which includes those checks. `complete` becomes true when the scan has failed, or has completed with every slow source in. Until then, the `Retry-After` header says when to read it again. Credits: Free. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | The scan’s `id`, returned by POST /scans. | | `checks` | query | no | The checks the scan was asked for; `links.self` includes them. Any of, separated by commas: `ai`, `readiness`, `popularity`, `tech`, `dns`, `trust`, `summary`, `history`. | Answers: 200 `Scan` (The scan as it is now.) Errors: `scan_not_found`, `scan_expired`, `invalid_parameter`, `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `rate_limited`, `internal_error` ### GET /usage Retrieve your usage. Returns the credits your account has used and has left this month (UTC), the key’s own limit if it has one, and the price of each check. Credits: Free. Answers: 200 `Usage` (This month’s credits.) Errors: `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `rate_limited`, `internal_error` ## Checks Ask only for the checks you need: `?checks=ai,dns` in a query, or `"checks": ["ai", "dns"]` in a body. Leave it out to ask for every check. A new scan runs only what its checks need, so fewer checks are faster and cost less. The result has a field for each check asked, and every other field is null. | Check | Fills | Run by the scan of | Stored | New scan | | --- | --- | --- | --- | --- | | `ai` | `ai` | AI Website Detector | 1 | 3 | | `readiness` | `readiness` | AI Crawler Checker | 1 | 4 | | `popularity` | `popularity` | Website Traffic Checker | 1 | 1 | | `tech` | `tech` | Tech Stack Checker | 1 | 3 | | `dns` | `dns` | Tech Stack Checker | 1 | 1 | | `trust` | `trust`, `score` | Website Trust Checker | 2 | 6 | | `summary` | `summary` | Website Summary | 2 | 5 | | `history` | `history` | Domain History | 1 | 2 | A request costs credits for each check it asks for, and never more than every check at once: 3 from a stored result, or 15 with a new scan. ## Batches `POST /sites/batch` takes `{"domains": [...]}` with 1 to 50 domains or URLs, and optionally `checks` and `maxAgeDays`. `POST /scans/batch` takes 1 to 10, and optionally `checks` and `again`. Both return `{"data": [...]}` with one item per unique domain, in the order each first appears. In a lookup batch, an item has `input` (the value you sent), `domain`, and either `result` or `error`. In a scan batch, an item is the site's scan, or `input`, `domain` and `error` when the site can't be scanned. An `error` has `code` and `message`, and `domain` is null when the input isn't a domain. A scan batch returns 202, or 200 when every item is complete. ## The result | Field | Type | Meaning | | --- | --- | --- | | `domain` | string | The site’s domain, as we read it ("example.com"). | | `checks` | array of one of: ai, readiness, popularity, tech, dns, trust, summary, history | The checks this result holds: those asked that the scan answered. | | `scanId` | string | The scan that made this result. | | `scannedAt` | date-time | When the scan read the site. | | `complete` | boolean | False while slow sources (`pending`) are still being read. | | `pending` | array of string | The slow sources still being read, if any. | | `limited` | object or null | Set when the page itself couldn’t be read; the result then comes from outside records. | | `limited.reason` | one of: blocked, robots, private | `blocked` (bot protection turned us away), `robots` (robots.txt keeps us out) or `private` (the site asks visitors to sign in). | | `limited.message` | string | What that means, in plain words. | | `finalUrl` | string | The address the scan ended at, after redirects. | | `resultUrl` | string | The result on our site, with every section and its evidence. | | `versions` | object | The signature list and score formula the result was made with (/changelog). | | `versions.signatures` | string | The signature list’s version. | | `versions.score` | string | The score formula’s version. | | `ai` | object or null | AI website builder traces (check `ai`). | | `ai.level` | one of: high, medium, none | `high`: an AI builder’s own trace; `medium`: a combination of its weaker traces only; `none`: no traces found. | | `ai.builders` | array of object | Each builder whose traces we found; empty when none. | | `ai.builders[].id` | string | Our id for the builder ("lovable"). | | `ai.builders[].name` | string | The builder’s name ("Lovable"). | | `ai.builders[].kind` | one of: ai, general | `ai` for AI builders, `general` for other site builders. | | `ai.builders[].url` | string | The builder’s own site. | | `ai.builders[].strength` | one of: signature, combination | `signature`: one trace of its own; `combination`: weaker traces that count only together. | | `ai.builders[].combination` | string or null | For a combination, what its traces mean together. | | `ai.builders[].evidence` | array of object | Where each trace was seen, with the exact line. | | `ai.builders[].evidence[].where` | string | Where we saw it: "index.html, line 14", "response header", "address". | | `ai.builders[].evidence[].snippet` | string | The element, header or excerpt as it appears. | | `ai.builders[].evidence[].match` | string | The part of the snippet that matched. | | `ai.builders[].evidence[].masked` | boolean | True when email addresses and phone numbers in the snippet are masked. | | `ai.patterns` | array of object | Design patterns common on AI-built pages; they never set the level on their own. | | `ai.patterns[].id` | string | Our id for the pattern. | | `ai.patterns[].name` | string | The pattern’s name. | | `ai.patterns[].note` | string | Why it is weak evidence on its own. | | `ai.patterns[].evidence` | array of object | Where it was seen. | | `ai.patterns[].evidence[].where` | string | Where we saw it: "index.html, line 14", "response header", "address". | | `ai.patterns[].evidence[].snippet` | string | The element, header or excerpt as it appears. | | `ai.patterns[].evidence[].match` | string | The part of the snippet that matched. | | `ai.patterns[].evidence[].masked` | boolean | True when email addresses and phone numbers in the snippet are masked. | | `readiness` | object or null | How ready it is for AI tools, out of 100 (check `readiness`). A result made before we checked AI access takes it from a newer check when there is one (its `checkedAt` says when), and is null otherwise. | | `readiness.answer` | string | The answer in a sentence ("Ready for AI: it scores 82 of 100."). | | `readiness.note` | string | What it lets AI crawlers do, in a sentence: the site’s choice, never scored. | | `readiness.score` | integer or null | Out of 100; null when too little could be checked to say. | | `readiness.version` | string | The readiness formula’s version (/changelog). | | `readiness.checkedAt` | date-time | When AI access was checked: the scan’s own date, or for a result made before we checked AI access, the newer check that answers in its place. | | `readiness.level` | object or null | The highest level it reaches; null when it reaches none. | | `readiness.level.id` | string | The level’s id ("readable"). | | `readiness.level.name` | string | Its name ("Readable"). | | `readiness.level.meaning` | string | What reaching it means, in a sentence. | | `readiness.levels` | array of object | Every level, lowest first; one counts once its checks and every level before it are met. | | `readiness.levels[].id` | string | `findable`, `readable`, `ai-friendly` or `agent-ready`; new levels may appear. | | `readiness.levels[].name` | string | The level’s name. | | `readiness.levels[].reached` | boolean | True when the site reaches it. | | `readiness.next` | object or null | The first level not reached and what it needs; null once every level is. | | `readiness.next.id` | string | The level’s id. | | `readiness.next.name` | string | Its name. | | `readiness.next.needs` | array of string | The keys of the checks it still needs, as `checks` lists them. | | `readiness.comparison` | string or null | Where the score stands among the most visited sites we read, in a sentence; null without a score. | | `readiness.partial` | boolean | True when some checks couldn’t be made or are still checking; the rest is scaled to 100. | | `readiness.checks` | array of object | Each check that counts, as the score reads it. | | `readiness.checks[].key` | string | A stable key: `text`, `listing`, `facts`, `structured`, `sitemap`, `robots`, `agents` (every file for AI agents at once: one found is enough), `llms`, `links` or `markdown`; new keys may appear. | | `readiness.checks[].group` | one of: find, read, agents | `find` (can AI find its pages?), `read` (can AI read it?) or `agents` (can agents use it?). | | `readiness.checks[].label` | string | The check’s name ("Structured data"). | | `readiness.checks[].state` | one of: found, partial, none, info, unknown | `found`, `partial`, `none`, `info` or `unknown` (we couldn’t check it). | | `readiness.checks[].value` | string | What we found, in words. | | `readiness.checks[].evidence` | array of object | Where it was seen, with the exact line. | | `readiness.checks[].evidence[].where` | string | Where we saw it: "index.html, line 14", "response header", "address". | | `readiness.checks[].evidence[].snippet` | string | The element, header or excerpt as it appears. | | `readiness.checks[].evidence[].match` | string | The part of the snippet that matched. | | `readiness.checks[].evidence[].masked` | boolean | True when email addresses and phone numbers in the snippet are masked. | | `readiness.checks[].points` | integer | The points it earns. | | `readiness.checks[].max` | integer | The most it can earn. | | `readiness.checks[].met` | boolean | True when it earns its full points. | | `readiness.checks[].unchecked` | boolean | True when we couldn’t check it, or it is still checking: it is left out of the score. | | `readiness.checks[].optional` | boolean | True for what few sites have yet; it is worth few points. | | `readiness.checks[].fix` | object or null | What to do about it; null when it is met or couldn’t be checked. | | `readiness.checks[].fix.gain` | integer | The points fixing it would add to the score. | | `readiness.checks[].fix.why` | string | Why it matters. | | `readiness.checks[].fix.todo` | string | What to do, in a line. | | `readiness.checks[].fix.prompt` | string | A prompt for an AI coding assistant to fix it, with the site’s address and what we found. | | `readiness.fixPrompt` | string or null | The fixes that aren’t optional, most points first, as one prompt for an AI coding assistant that ends with where to check again; the optional ones once only they are left. Null when there is nothing to fix. | | `readiness.crawlers` | array of object | Every AI crawler we know, as the site’s robots.txt treats it; never scored. | | `readiness.crawlers[].name` | string | The crawler, as its maker writes it ("GPTBot"). | | `readiness.crawlers[].company` | string or null | The AI company it belongs to, for those we follow. | | `readiness.crawlers[].role` | one of: training, answers | What it fetches pages for: `training` models, or `answers` (AI assistants and AI search). | | `readiness.crawlers[].access` | one of: out, some, in | `out` of the whole site, kept from `some` paths, or let `in`. | | `readiness.crawlers[].line` | integer or null | The robots.txt line of the group that decides it; null when no group names it. | | `popularity` | object or null | Popularity from public ranked lists (check `popularity`). | | `popularity.band` | string | The best band any list gives the site ("Top 100K"), or "Outside top 1M". | | `popularity.bestRank` | integer or null | The best rank behind the band; null when no list ranks the site. | | `popularity.ranks` | array of object | Each list we read, with the site’s rank on it. | | `popularity.ranks[].list` | string | The list’s name ("Chrome UX Report"). | | `popularity.ranks[].rank` | integer or null | The rank, or null when the list doesn’t rank the site. | | `popularity.ranks[].isBucket` | boolean | True when the rank is the edge of a bucket, not an exact position. | | `popularity.ranks[].rankedAs` | string or null | The parent name the list ranks, when not the site itself. | | `popularity.ranks[].asOf` | string | When the list was produced, as it states it. | | `popularity.ranks[].measures` | string | What the list counts. | | `popularity.ranks[].url` | string | Where the list is published. | | `popularity.ranks[].license` | object or null | The list’s license, when its terms ask for attribution: keep it when you republish. | | `popularity.ranks[].license.name` | string | The license’s name ("CC BY 3.0"). | | `popularity.ranks[].license.url` | string | The license’s text. | | `tech` | object or null | The technology it runs on (check `tech`). | | `tech.technologies` | array of object | Each technology the site shows, with its evidence. | | `tech.technologies[].id` | string | Our id for the technology. | | `tech.technologies[].name` | string | Its name ("Next.js"). | | `tech.technologies[].category` | string | Such as "framework", "cms", "analytics", "hosting". | | `tech.technologies[].evidence` | array of object | Where it was seen, with the exact line. | | `tech.technologies[].evidence[].where` | string | Where we saw it: "index.html, line 14", "response header", "address". | | `tech.technologies[].evidence[].snippet` | string | The element, header or excerpt as it appears. | | `tech.technologies[].evidence[].match` | string | The part of the snippet that matched. | | `tech.technologies[].evidence[].masked` | boolean | True when email addresses and phone numbers in the snippet are masked. | | `tech.network` | object or null | A network, never a place: many networks answer from everywhere. | | `tech.network.asn` | integer | The autonomous system number, such as 13335. | | `tech.network.name` | string | The organisation announcing the address ("Cloudflare, Inc."). | | `tech.network.prefix` | string or null | The routed block holding the address ("104.16.16.0/20"). | | `tech.network.source` | string | Who answered. | | `tech.network.checkedAt` | date-time | When we asked. | | `dns` | object or null | Its DNS records (check `dns`). | | `dns.read` | boolean | False when the lookup failed: then there are no records to give. | | `dns.records` | array of object | Each record we read. | | `dns.records[].name` | string | The name asked about: the domain, its www host, or a policy entry such as `_dmarc`. | | `dns.records[].type` | one of: A, AAAA, CNAME, MX, TXT, NS, CAA, SVCB, HTTPS | The record type. SVCB and HTTPS come only from `_agents`, where a site names its AI agents. | | `dns.records[].value` | string | The record’s value, as the resolver gave it. | | `dns.dnssec` | boolean or null | Whether the resolver validated the answer with DNSSEC; null when that didn’t answer. | | `dns.incomplete` | array of string | The questions ("example.com TXT") that didn’t answer whole: a record missing from one of them may still exist. | | `dns.source` | string or null | The resolver that answered. | | `dns.checkedAt` | date-time or null | When we asked. | | `dns.network` | object or null | The network announcing the site’s address, never a place. | | `dns.network.asn` | integer | The autonomous system number, such as 13335. | | `dns.network.name` | string | The organisation announcing the address ("Cloudflare, Inc."). | | `dns.network.prefix` | string or null | The routed block holding the address ("104.16.16.0/20"). | | `dns.network.source` | string | Who answered. | | `dns.network.checkedAt` | date-time | When we asked. | | `trust` | object or null | Trust signals (check `trust`). | | `trust.checks` | array of object | Each signal we looked for, found or not. | | `trust.checks[].key` | string | A stable key such as `privacyPolicy` or `domainAge`; new keys may appear. | | `trust.checks[].label` | string | The signal’s name ("Privacy policy"). | | `trust.checks[].state` | one of: found, partial, none, info, unknown | `found`, `partial`, `none` (not found; absence is never an accusation), `info` (a fact) or `unknown` (couldn’t check). | | `trust.checks[].value` | string | What we found, in words. | | `trust.checks[].kind` | one of: measured, sourced, interpreted | `measured` by us, `sourced` from a named source, or `interpreted` from what we read. | | `trust.checks[].source` | string | Where it came from: our scan or a named source. | | `trust.checks[].evidence` | array of object | Where it was seen, when the page or a record shows it. | | `trust.checks[].evidence[].where` | string | Where we saw it: "index.html, line 14", "response header", "address". | | `trust.checks[].evidence[].snippet` | string | The element, header or excerpt as it appears. | | `trust.checks[].evidence[].match` | string | The part of the snippet that matched. | | `trust.checks[].evidence[].masked` | boolean | True when email addresses and phone numbers in the snippet are masked. | | `trust.checks[].pending` | boolean | True while a slow source is still being read. | | `score` | object or null | How established it looks, out of 100 (check `trust`, with the signals). | | `score.total` | integer or null | Out of 100; null when too little could be read to score fairly. | | `score.band` | string | The band the total falls in. | | `score.withheld` | boolean | True when too little could be read; `total` is null then. | | `score.version` | string | The score formula’s version (/changelog). | | `score.parts` | array of object | The parts the total is made of, each with its points. | | `score.parts[].key` | string | The part, such as `transparency`, `trackRecord`, `popularity`, `security` or `consistency`. | | `score.parts[].label` | string | The part’s name. | | `score.parts[].points` | number | The points it earned. | | `score.parts[].max` | number | The most it could earn from what we could check. | | `score.parts[].reasons` | array of string | Why points were given or withheld, one short phrase each. | | `score.parts[].checked` | boolean | False when none of its inputs could be checked. | | `score.missing` | array of string | Inputs we couldn’t check; the rest is scaled to 100. | | `summary` | object or null | What it is and who is behind it (check `summary`). | | `summary.title` | string or null | The page’s own title. | | `summary.description` | string or null | The page’s own description. | | `summary.siteName` | string or null | The name the site gives itself. | | `summary.language` | string or null | The page’s language code ("en"). | | `summary.kind` | object or null | The kind of site we read it as (a shop, a blog, an app…); null when unclear. | | `summary.kind.id` | string | Our id for the kind ("shop"). | | `summary.kind.label` | string | Its name. | | `summary.kind.confidence` | one of: high, medium | How sure the reading is. | | `summary.summary` | object or null | An automatic summary whose quotes we checked against the page; null when there is none. | | `summary.summary.text` | string | Two or three sentences: what the site is and offers, as the page presents it. | | `summary.summary.category` | string | What kind of site the model read it as. | | `summary.summary.offerings` | array of string | What it offers, in short phrases. | | `summary.summary.audience` | string or null | Who it is for, when the page says. | | `summary.summary.model` | string | The model that wrote it. | | `summary.summary.writtenAt` | date-time | When it was written. | | `summary.identity` | object or null | Who is behind the site, by independent sources. | | `summary.identity.names` | array of object | Each name a source gives for who is behind the site. | | `summary.identity.names[].name` | string | The name as the source gives it. | | `summary.identity.names[].source` | string | Where it comes from: `structured data`, `copyright line`, `Wikidata` or `certificate`. | | `summary.identity.agreeing` | array of string | The sources whose names agree, when at least two do. | | `summary.wikidata` | array of object | The Wikidata items that name the domain as their official website. | | `summary.wikidata[].id` | string | The item’s id ("Q52"). | | `summary.wikidata[].label` | string | Its label. | | `summary.wikidata[].description` | string or null | Its short description. | | `summary.wikidata[].types` | array of string | What it is an instance of, in English ("search engine"). | | `summary.wikidata[].inceptionYear` | integer or null | The year it was founded or launched. | | `summary.wikidata[].url` | string | Its page on Wikidata. | | `summary.social` | array of object | The profiles the home page links. | | `summary.social[].network` | string | The network ("github", "linkedin"). | | `summary.social[].handle` | string | The account as its address names it ("acme", "company/acme"). | | `summary.social[].url` | string | The profile’s address. | | `summary.aiPolicy` | object or null | What the site says about AI crawlers using its content. | | `summary.aiPolicy.trainingBlocked` | array of string | AI training crawlers robots.txt keeps out of the whole site. | | `summary.aiPolicy.trainingPartly` | array of string | Training crawlers it gives rules for some paths only. | | `summary.aiPolicy.assistantsBlocked` | array of string | AI assistants (fetching pages for a person) it keeps out of the whole site. | | `summary.aiPolicy.everyoneBlocked` | boolean | True when every crawler is kept out, AI ones included. | | `summary.aiPolicy.contentSignal` | string or null | Its Content Signals line, as written ("search=yes, ai-train=no"). | | `summary.aiPolicy.noai` | boolean | True when a `noai` robots tag or header is set. | | `summary.llmsTxt` | object or null | The site’s llms.txt (llmstxt.org), when it has one. | | `summary.llmsTxt.url` | string | Where it is. | | `summary.llmsTxt.title` | string | Its title. | | `summary.llmsTxt.summary` | string or null | The summary it writes for AI assistants. | | `summary.iconUrl` | string or null | Our copy of the site’s icon. | | `history` | object or null | Its domain’s history (check `history`). | | `history.registration` | object or null | The domain registry’s record; null when it couldn’t be read. | | `history.registration.registeredAt` | date-time or null | When the domain was registered; null when the registry doesn’t say. | | `history.registration.lastChangedAt` | date-time or null | When the record last changed. | | `history.registration.registrar` | string or null | The registrar, as the registry names it. | | `history.registration.recordFor` | string or null | The registered name, when the record is for a parent name. | | `history.registration.source` | string | The registry server that answered (RDAP or WHOIS). | | `history.registration.checkedAt` | date-time | When we asked. | | `history.firstCertificate` | object or null | Certificate Transparency: when a certificate for the domain was first issued. | | `history.firstCertificate.firstSeen` | date-time or null | The earliest certificate’s start date; null when there were too many to read. | | `history.firstCertificate.count` | integer or null | How many certificates the logs list, when read in full. | | `history.firstCertificate.source` | string | The log search that answered. | | `history.firstCertificate.checkedAt` | date-time | When we asked. | | `history.archive` | object or null | The Internet Archive’s record of the site. | | `history.archive.firstCapture` | date-time or null | The web archive’s first capture of the site. | | `history.archive.firstCaptureUrl` | string or null | A link to that capture, to look at. | | `history.archive.captureYears` | array of integer | The years with at least one capture, oldest first. | | `history.archive.source` | string | The archive that answered. | | `history.archive.checkedAt` | date-time | When we asked. | | `notes` | array of string | Limits of this scan, in plain words. | Within v1 we may add fields and values, but never remove or rename them, so ignore what you don’t know. ## Credits | What | Credits | | --- | --- | | Checks from a stored result (GET /sites/{domain}, a site found by POST /sites/batch, or a scan answered by a result from the last day) | Each check’s stored price, at most 3 | | Checks with a new scan (each new scan in a batch too) | Each check’s scan price, at most 15 | | Reading a scan (polling), reading usage, a site a batch doesn’t find, a 304, a replayed idempotent response, a request that fails | 0 | Free during launch: every account gets 300 credits each calendar month (UTC), shared by its keys, and can start up to 25 new scans a day (`scan_limit_reached`). For more credits, ask at https://checkanysite.com/account/usage#more-credits; extra credits are free during launch too. Paid plans will come later, with at least 7 days' notice by email. Responses to requests that cost credits include `X-Credits-Cost`, `X-Credits-Remaining` and `X-Credits-Reset`, and every response includes `X-Request-Id`. `GET /sites/{domain}` returns an `ETag`: send it back in `If-None-Match`, and an unchanged result returns 304 with no body and no credits. ## Rate limits and retries Each key can make 60 requests every 60 seconds. Responses to authenticated requests include `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (the Unix time when the window ends). Several limits return 429, so branch on the error's `code`, not only its status. | Response | Retry? | | --- | --- | | 429 `rate_limited` | Yes, after the seconds in `Retry-After`. | | 429 `quota_exceeded`, `key_limit_reached`, `scan_limit_reached` | No. A monthly or daily limit is used up; retrying fails until it resets (the message says when), or until the key's limit is raised. | | 409 `idempotency_key_in_use` | Yes, a moment later. The first request with that key is still running. | | 503 `capacity` | Yes, after the seconds in `Retry-After`. Stored results still work meanwhile. | | 500 `internal_error` | Yes, with backoff: wait 1, 2, 4 and 8 seconds with a little jitter, for up to five attempts. | | Any other 4xx | No. Change the request first; the message says what to change. | - Polling a scan is free. Read its `links.self` as often as `Retry-After` asks, about every 3 seconds. - Send an `Idempotency-Key` with every POST request, the same one on each retry, so a retry never starts or charges for anything twice. ```js const RETRYABLE = new Set([ 'idempotency_key_in_use', 'rate_limited', 'capacity', 'internal_error', ]); async function callApi(path, init = {}, tries = 5) { // Send the same key with every try of a POST, so a retry never starts // or charges for a second scan. const idempotency = init.method === 'POST' ? { 'Idempotency-Key': crypto.randomUUID() } : {}; for (let attempt = 1; ; attempt += 1) { const response = await fetch(`https://api.checkanysite.com/v1${path}`, { ...init, headers: { Authorization: `Bearer ${process.env.CHECKANYSITE_API_KEY}`, ...idempotency, ...init.headers, }, }); if (response.ok) return response.json(); const { error } = await response.json(); if (!RETRYABLE.has(error.code) || attempt === tries) { throw new Error(`${error.code}: ${error.message} (${error.requestId})`); } // Wait as long as Retry-After asks, or back off: 1, 2, 4… seconds. const retryAfter = Number(response.headers.get('retry-after')); const seconds = retryAfter || 2 ** (attempt - 1); const delay = seconds * 1000 + Math.random() * 250; await new Promise((resolve) => setTimeout(resolve, delay)); } } const site = await callApi('/sites/wikipedia.org?checks=ai'); ``` ## Idempotency Every POST endpoint accepts an `Idempotency-Key` header (1 to 255 characters, such as a UUID), kept for 24 hours per API key. The same key with the same body returns the first response, with `Idempotent-Replayed: true`, and is never charged twice. The same key with a different body returns 422 `idempotency_key_reused`. The same key while the first request is still running returns 409 `idempotency_key_in_use`; retry a moment later. ## Errors Every error has this shape, with a stable `code` to branch on: ```json { "error": { "code": "quota_exceeded", "message": "The account has used all its credits for this month.", "docs": "https://docs.checkanysite.com/errors#quota_exceeded", "requestId": "req_…" } } ``` | Code | Status | Retry? | Meaning | What to do | | --- | --- | --- | --- | --- | | `missing_api_key` | 401 | No | The request has no API key. | Send your key in the Authorization header as `Bearer cas_live_…`, or in an `X-API-Key` header. Create a key at https://checkanysite.com/account/keys. | | `invalid_api_key` | 401 | No | The API key doesn’t exist, or it was copied incorrectly. | Copy the key again from where you stored it. We show a key only once, so create a new one if you no longer have it. | | `revoked_api_key` | 401 | No | The API key was revoked and no longer works. | Use another key, or create a new one at https://checkanysite.com/account/keys. | | `expired_api_key` | 401 | No | The API key has expired: it reached the expiry date set when it was created, or the end of its grace period after a rotation. | Use the key that replaced it, or create a new one at https://checkanysite.com/account/keys. To avoid downtime, rotate keys before they expire. | | `invalid_json` | 400 | No | The request body isn’t a valid JSON object. | Send a JSON object, such as `{"domain": "example.com"}`, with `Content-Type: application/json`. | | `invalid_parameter` | 400 | No | A parameter is missing, unknown or has a value the API doesn’t accept. `param` names it. | Check the parameter’s name, type and allowed values in the API reference. | | `invalid_domain` | 400 | No | The address isn’t a public domain name we can check, such as an IP address, a private name or a typo. | Send a domain, such as `example.com`. A full URL also works; we ignore its path. | | `unsupported_media_type` | 415 | No | The request body has a content type other than JSON. | Send the body as JSON, with `Content-Type: application/json`. | | `request_too_large` | 413 | No | The request body is larger than the API accepts. | Send only the documented fields. | | `not_found` | 404 | No | There is no API endpoint at this path. | Check the path in the API reference. Every endpoint is under the base URL, such as /v1/sites/{domain}. | | `method_not_allowed` | 405 | No | The endpoint exists but doesn’t accept this HTTP method. The `Allow` header lists the methods it accepts. | Use one of the methods in the `Allow` header. | | `no_result` | 404 | No | We have no stored result for this site with these checks, or none as recent as you asked for. | Scan the site with POST /v1/scans. This request cost nothing. | | `scan_not_found` | 404 | No | There is no scan with this ID. | Use the `id` returned when you started the scan. | | `scan_expired` | 410 | No | This scan is older than the results we keep, so its result is no longer available. We keep each site’s latest result. | Get the site’s latest result with GET /v1/sites/{domain}, or scan it again with POST /v1/scans. | | `read_only_key` | 403 | No | This key is read-only. It can read stored results but can’t start a scan. | Get the stored result with GET /v1/sites/{domain}, or use a key with full access to start scans. | | `browser_request` | 403 | No | The API accepts requests from servers only, because a key sent from a browser is visible to anyone who opens the page. | Call the API from your server or a serverless function, and keep the key there. | | `site_refused` | 422 | No | We don’t check this kind of site, such as adult websites. | There’s nothing to do. This request cost nothing. | | `idempotency_key_reused` | 422 | No | This `Idempotency-Key` was sent with a different request in the last 24 hours. Each key belongs to one request. | Send a new key, such as a new UUID, with each new request. Reuse a key only to retry the same request. | | `idempotency_key_in_use` | 409 | Yes, with backoff | A request with this `Idempotency-Key` is still being processed. | Retry in a second with the same key. You’ll get the first request’s response, without a second charge. | | `rate_limited` | 429 | Yes, after Retry-After | This key has made more requests than its rate limit allows. | Wait the number of seconds in the `Retry-After` header, then retry. Spread your requests out over time. | | `quota_exceeded` | 429 | No | The account has used all its credits for this month. | Wait for the reset on the first of the month (UTC), or ask for more credits at https://checkanysite.com/account/usage#more-credits; extra credits are free during launch. Stored results cost the fewest credits. | | `key_limit_reached` | 429 | No | This key has used the monthly credit limit set for it. | Raise the key’s limit at https://checkanysite.com/account/keys, use another key, or wait for the reset on the first of the month (UTC). | | `scan_limit_reached` | 429 | No | The account has started its 25 new scans for today (UTC). | Get stored results with GET /v1/sites/{domain}, or start the scan after 00:00 UTC. This request cost nothing. | | `capacity` | 503 | Yes, after Retry-After | The API can’t start new scans right now. Stored results still work. | Get stored results with GET /v1/sites/{domain}, or start the scan later; the message says when. | | `internal_error` | 500 | Yes, with backoff | Something went wrong on our side. This request cost nothing. | Retry in a minute. If the error continues, contact us and include the `requestId`. | ## Versioning The version is part of the path: `/v1`. Within v1 we only add fields, enum values and error codes, so ignore what you don't know. We announce anything we retire at least 6 months ahead, on https://checkanysite.com/changelog (marked API) and in `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers on the responses it affects. ## MCP server Connect any MCP client to `https://checkanysite.com/mcp` (Streamable HTTP). Clients that sign in with OAuth, such as Claude and ChatGPT, need only this URL; others send the same key in the `Authorization` header. A client that accepts only a URL and can't sign in can add the key as `?key=` to this URL (no other endpoint accepts it); keep that URL as private as the key. Each tool takes a `source` argument: `stored` uses the Lookup API, and `scan` uses the Scan API. The tools: - `check_website`: Everything we know about a website in one call: AI website builder traces, how ready it is for AI tools, popularity, the technology it runs on, trust signals and score, what it is, and its domain history, each finding with its evidence or source (its DNS records ride along in the structured result). Use it when someone asks what a site is, who is behind it, or whether it looks established. Report the findings with their evidence; never call a site safe, a scam or fake. Costs 3 credits from our stored result (the default), 15 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `detect_ai_builder`: Whether a website shows traces of an AI website builder (such as Lovable, Bolt, v0, Base44 or Replit) or of another site builder, with the evidence line for each. Use it when someone asks if a site was made with AI or "vibe coded". A trace says how the site was built, nothing about who runs it. Report the findings with their evidence; never call a site safe, a scam or fake. Costs 1 credit from our stored result (the default), 3 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `get_ai_readiness`: How easily AI tools (assistants, AI search and agents) can find, read and use a website: a score out of 100 from checks such as its text without scripts, a search listing, a title and description, structured data, a sitemap, llms.txt and files for AI agents (an MCP server card, an API catalog, agent skills), each with its evidence; the level it reaches; and for each gap what to do, with a prompt for an AI coding assistant. It also says which AI crawlers its robots.txt lets in or keeps out: the site's choice, never scored. Use it when someone asks whether AI can find, read or cite a site, or how to make theirs ready for AI. Costs 1 credit from our stored result (the default), 4 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `get_popularity`: How popular a website is, from public ranked lists (Chrome UX Report, Majestic Million): a band such as "Top 100K" and each list's rank. Never a visit count. Costs 1 credit from our stored result (the default), 1 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `get_tech_stack`: The technologies a website shows (frameworks, site builders, shop systems, analytics, servers) with the evidence for each, and the network announcing its address. Costs 1 credit from our stored result (the default), 3 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `get_dns_records`: A domain's DNS records as we read them over DNS over HTTPS (A, AAAA, CNAME, MX, TXT, NS, CAA, and the AI agents a site names under _agents), whether its answer was validated with DNSSEC, and the network announcing its address. Use it when someone asks where a domain points, who handles its email, or what its TXT records say. Costs 1 credit from our stored result (the default), 1 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `get_trust_signals`: Signals about how established and reachable a website is: contact and company details, policy pages, domain age, HTTPS and security setup, and a score out of 100 for how established it looks. Signals for a person to weigh, never a verdict about intent. Report the findings with their evidence; never call a site safe, a scam or fake. Costs 2 credits from our stored result (the default), 6 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). - `get_domain_history`: A domain's history from public records: when it was registered (domain registry), its first TLS certificate (certificate logs) and when the web archive first captured it. Costs 1 credit from our stored result (the default), 2 with source "scan" when it has to scan (free monthly credits; get a key at https://checkanysite.com/account/keys). Agents can find the server on their own: its card is at `https://checkanysite.com/mcp/server-card` (MCP Server Cards, SEP-2127) and is listed in our AI Catalog at `https://checkanysite.com/.well-known/ai-catalog.json`. The API catalog at `https://checkanysite.com/.well-known/api-catalog` (RFC 9727) lists this API, its OpenAPI document and this reference. ## Python SDK The `checkanysite` package covers both APIs. It retries 429 `rate_limited`, 409, 5xx and connection errors as `Retry-After` asks, and sends an `Idempotency-Key` with every POST request. Errors are exceptions such as `checkanysite.RateLimitError`, `AuthenticationError` and `NotFoundError`, each with `code`, `message` and `request_id`. The following function uses a stored result when there is one and scans the site otherwise: ```bash pip install checkanysite ``` ```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 ``` ## Agent skill ```bash npx skills add https://checkanysite.com ``` ## Rules for what you build - Show each finding with its evidence or source, and link to `resultUrl`. - Present findings as signals, not verdicts: never call a site safe, legitimate, a scam or fake. - Keep the license of any list you republish (`popularity.ranks[].license`).