API Docs
Create an asynchronous audit, poll its state, then fetch the bounded evidence result.
Contract
Base URL: https://generativemetrics.com. Version prefix: /api/v1. API keys are user-scoped and share the owning account's scan quota.
POST /api/v1/scanGET /api/v1/scan/{id}GET /api/v1/scan/{id}/resultGET /api/v1/scan/{id}/evidence-bundle(owning account: API key or browser session)
A machine-readable OpenAPI description of these endpoints is served at /openapi.json.
New evidence-first rows identify themselves with result_contract: "evidence-first". Historical rows retain their legacy compatibility response. Clients must branch on result_contract; they must not infer the contract from null score fields.
For POST /api/v1/scan, the request body must be valid JSON (malformed bodies return 400 invalid_request with message "request body must be valid JSON"). The submitted URL must not exceed 2,048 characters. Scan IDs in path parameters must be valid UUIDs.
Availability
These endpoints are live and documented. What is not open yet is self-serve access: API keys and webhooks require a paid plan, and paid checkout is currently closed.
Today you can read the contract, create a free account, and run web audits to see the exact result shape your integration will receive. Watch the pricing page for launch access.
With a paid plan: create a key in Settings (shown once, gm_live_) and authenticate with Authorization: Bearer <key> or x-api-key: <key>. API scans draw from the same monthly pool as dashboard scans (125 URL audits/mo Agency, 625/mo Firm) and are rate-limited per account per minute. 1 site audit = up to 25 URL audits; single-page audits draw from the same URL-audit pool. Idempotency and webhooks work as documented below.
Test keys (gm_test_) are available on beta-grant accounts against fixture.generativemetrics.com only; there is no general sandbox environment today. Every error uses the JSON envelope { error, message } with the HTTP status below:
| Condition | Response |
|---|---|
| Missing key | 401 unauthorized |
| Unknown key | 401 invalid_api_key |
| Revoked key | 401 revoked_api_key |
| No paid entitlement | 403 plan_required |
| Monthly scan limit reached | 403 plan_required |
| Unknown scan id or scan owned by another account | 404 not_found |
| Result requested before completion or after failure | 409 result_not_ready |
| Evidence bundle requested without a session | 401 unauthorized |
| Evidence bundle missing | 404 not_found |
| Stored evidence bundle fails identity or integrity verification | 409 evidence_bundle_verification_failed |
| Unexpected server failure | 500 internal_error |
Authentication
Send either Authorization: Bearer <API_KEY> or x-api-key: <API_KEY>. API access is plan-gated.
Quickstart
Create scan:
curl -X POST "https://generativemetrics.com/api/v1/scan" \
-H "Authorization: Bearer gm_live_xxx" \
-H "Idempotency-Key: req_20260726_001" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'Poll status:
curl "https://generativemetrics.com/api/v1/scan/<scan_id>" \
-H "Authorization: Bearer gm_live_xxx"Fetch completed result:
curl "https://generativemetrics.com/api/v1/scan/<scan_id>/result" \
-H "Authorization: Bearer gm_live_xxx"Public quick check (unauthenticated)
POST /api/public-probe runs one bounded page observation without an account or API key. It accepts a JSON body {"url": "https://example.com"} and is rate-limited to 5 requests/minute and 30 requests/day per IP, with a global cap of 1,000 requests/day.
Quick check:
curl -X POST "https://generativemetrics.com/api/public-probe" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'The response returns recorded observations only — not a verdict and not an audit. It records one request at one moment: page reachability, X-Robots-Tag, robots.txt for the observed generic (*) group, llms.txt, and raw HTML signals. The durationMs property measures target-fetch time only (the network round-trip time to fetch the target page; it excludes downstream processing and queue wait times). A signal that is not observed is not proof that it does not exist. For the full evidence-first result, use POST /api/v1/scan.
The same bounded observation is available to MCP clients at /mcp (Streamable HTTP, tools quick_check and audit_site — the latter requires an API key) under the same per-IP limits.
MCP server (agents)
https://generativemetrics.com/mcp is a public Model Context Protocol server over Streamable HTTP (JSON-RPC 2.0, current stateless protocol version with a legacy fallback). No account, API key, or OAuth is required: add the URL to any MCP client, or call it directly. Two tools are exposed — quick_check (public) and audit_site (API key required; starts a full audit and returns the scan id) — returning the same recorded observations as the public quick check under the same per-IP limits and shared global daily cap. GET /mcp returns 405 by design; use POST.
Initialize:
curl -X POST "https://generativemetrics.com/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0.0"}}}'Run the quick check:
curl -X POST "https://generativemetrics.com/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"quick_check","arguments":{"url":"https://example.com"}}}'Tool output is recorded observations only — not a verdict and not an audit; a signal that is not observed is not proof that it does not exist. A machine-readable capability description is published at /.well-known/mcp.json.
Evidence-first result shape
The public projection excludes captured page bodies, non-allowlisted response headers, internal debug payloads, trust scores, and legacy metric objects. A completed evidence-first row with an invalid summary fails closed instead of falling back to the legacy response.
{
"success": true,
"scan": {
"result_contract": "evidence-first",
"status": "completed",
"audit_summary": {
"verdict": "FAIL",
"run_state": "COMPLETE",
"audit_profile": { "...": "..." },
"finding_counts": {
"blockers": 1,
"warnings": 0,
"informational": 1,
"experimental": 0
},
"limitations": ["..."]
},
"score_role": "not_calculated",
"secondary_score": null,
"evidence_bundle": {
"bundle_id": "...",
"bundle_sha256": "...",
"protocols": ["..."],
"findings": ["..."],
"remediations": ["..."]
},
"evidence_verification": {
"counts": {
"verified": 1,
"not_verified": 0,
"indeterminate": 0
},
"results": ["..."]
}
}
}Evidence bundle download
GET /api/v1/scan/{id}/evidence-bundle serves the sealed evidence bundle for a scan as a JSON attachment. It returns the stored EvidenceBundle artifact itself, not the public projection used by /result. The response carries Content-Disposition: attachment; filename="gm-evidence-<id>.json" and X-Evidence-Bundle-Sha256 with the sealed bundleSha256 so the download can be hashed and compared.
bundleSha256 is the lowercase hex SHA-256 over the bundle's canonical JSON form: object keys recursively sorted lexicographically, no whitespace, UTF-8 — computed over the full bundle with the bundleSha256 field itself removed. To verify a download, canonicalise the parsed JSON this way and hash the result; an in-browser digest checker (no authentication of who produced the file) is available at /verify.
Authentication: accepts either a valid account API key (Authorization: Bearer gm_live_... or x-api-key) or an active browser session belonging to the account that owns the scan. Requests without authentication or keys for other accounts are rejected.
API key:
curl "https://generativemetrics.com/api/v1/scan/<scan_id>/evidence-bundle" \
-H "Authorization: Bearer gm_live_xxx"Browser session:
curl "https://generativemetrics.com/api/v1/scan/<scan_id>/evidence-bundle" \
-H "Cookie: <session cookie>"Divergence behavior: an absent bundle returns 404 not_found, so the endpoint never leaks whether another account's bundle exists. A stored bundle that fails identity or integrity verification returns 409 evidence_bundle_verification_failed instead of a partial download.
{
"schemaVersion": "...",
"protocolSetVersion": "...",
"bundleId": "...",
"scanId": "...",
"targetUrl": "https://example.com",
"auditProfile": { "...": "..." },
"createdAt": "2026-01-01T00:00:00.000Z",
"runState": "COMPLETE",
"verdict": "FAIL",
"artifacts": ["..."],
"protocolExecutions": ["..."],
"findings": ["..."],
"remediations": ["..."],
"verifications": ["..."],
"limitations": ["..."],
"bundleSha256": "..."
}Verdict and verification semantics
PASS: required protocols completed and no production blocker is open.FAIL: an evidence-linked production blocker is open.INCOMPLETE: required evidence was not fully obtained.ACCESS_BLOCKED(display name is case-derived:AI Access Blocked, orAll Readers Refusedwhen every access probe the audit sent was refused): the site refused at least one required AI system in a scan window where the scan proves a contrast fact — a served exemplar probe, or our published control request received an answer. A single refusal on its own is recorded as an observation, not this verdict.VERIFIED: the candidate satisfied the original remediation outcome.NOT_VERIFIED: required protocols ran, but the outcome was not satisfied.INDETERMINATE: the comparison was incompatible or incomplete.
Precedence
The audit verdict is decided in a fixed order. FAIL is checked first: any open production finding with severity BLOCKER makes the verdict FAIL even when required evidence is missing, so FAIL can coexist with run_state: "PARTIAL". Next, ACCESS_BLOCKED applies when a required AI system was refused and the refusal is contrast-proven: the same scan window shows the site answering reference requests (a served exemplar probe, or our published control request received an answer), the refusal was answered from the site’s origin (the refusal’s edge-cache headers are sealed and none of the present ones reads HIT), and an immediate confirmation re-probe in the same scan was refused the same way. A refusal that meets fewer of these conditions is not this verdict: on a required actor it resolves INCOMPLETE rather than PASS, and on a non-required actor it stays the uncorroborated WARNING observation. INCOMPLETE applies only when no blocker is open and at least one of the following holds: a required protocol execution ended FAILED or ABSTAINED, there are no required executions at all, or an actor the audit profile marks REQUIRED was not measured by access.actor-probe (ABSTAINED or NOT_APPLICABLE; CONTROL_TOKEN actors are excluded from the rule), or a required actor’s recorded refusal was not confirmed as above. Otherwise the verdict is PASS.
run_state is derived separately from the verdict: FAILED when the target's own acquisition.http execution failed; PARTIAL when a required execution did not complete or a required actor was not measured; COMPLETE otherwise. FAIL + PARTIAL is therefore a legal combination.
Severity mapping to know when reading findings: access.actor-probe executions are never required and cannot fail the audit by themselves. A probe denial of a REQUIRED actor that robots.txt does not corroborate is recorded as severity WARNING (pairs with a RESTORE_ACTOR_ACCESS rescan remediation) and cannot produce FAIL; the deterministic BLOCKER for a required actor is the robots.txt-corroborated denial (ROBOTS_DISALLOWS_REQUIRED_DISCOVERY_ACTOR from policy.robots). A required actor that could not be measured is recorded as an ACTOR_ACCESS_NOT_MEASURED INFO finding and drives INCOMPLETE through the executions, not through a blocker. A recorded-but-unconfirmed refusal on a required actor drives INCOMPLETE the same way; only the contrast-proven refusal resolves ACCESS_BLOCKED.
Idempotency and quotas
POST /api/v1/scan accepts Idempotency-Key. Reusing the same key and payload replays the original response and sets Idempotency-Replayed: true. Reusing it for a different payload returns 409 idempotency_key_reused.
A replay returns the quota snapshot from the first accepted request: the X-Quota-Limit, X-Quota-Remaining, and X-Quota-Reset headers and the quota object repeat that first call's usage, not the account's current usage. Read live quota from a new 202 response.
Accepted 202 responses include X-Quota-Limit, X-Quota-Remaining, and X-Quota-Reset. 429 responses include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After. Scan intake is rate-limited to 10 scans per minute per account.
Rate limits and intake quotas are coordinated across server instances using a distributed cache. If the primary cache service is temporarily unreachable, rate limiting falls back to local in-process tracking per instance to preserve availability. During such an outage, effective limits apply independently on each active instance until central coordination is restored.
Webhooks
Plan-gated events are scan.completed and scan.failed. Each delivery is a POST carrying X-GM-Webhook-Id (the event id), X-GM-Webhook-Timestamp (Unix seconds), and X-GM-Signature.
The signature is HMAC-SHA256 over the UTF-8 bytes of the string {timestamp}.{eventId}.{payload}, where payload is the raw request body exactly as sent. The header format is X-GM-Signature: t=<timestamp>,v1=<lowercase hex digest>: t repeats X-GM-Webhook-Timestamp and v1 is the HMAC digest. Reconstruct the base string from the headers and the raw body, then compare with a timing-safe equality check:
const base = `${timestamp}.${eventId}.${payload}`;
const expected = crypto.createHmac('sha256', webhookSecret).update(base, 'utf8').digest('hex');
const valid =
expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));Reject deliveries whose timestamp differs from the current time by more than 300 seconds. Failed deliveries are retried a few times in-process with backoff using the same X-GM-Webhook-Id. Retries are best-effort and are not durably persisted across a worker restart, so deduplicate by event id, process each event idempotently, and reconcile from the scan API if a delivery is missed.
Visibility run export
The experimental visibility panel (dashboard-only, not part of the v1 API above) carries a receipt export at GET /api/ai-visibility/runs/{id}/export?format=json|csv, authenticated by the owning account's browser session. It returns the run's full instrument record: what was asked (query text and prompt-template hash), what came back per engine (status, mention record, confidence label, model identity, token usage, citations with their provenance), and the failed asks labeled as failed — an instrument failure never masquerades as a provider answer.
The recompute promise: on every row that retains its reply text — success or failed-with-retained-reply — each row's raw_response_sha256 is a SHA-256 over the full raw provider answer (raw_response_full). You can recompute it offline — hash the exported full text yourself and compare. If they match, the answer shown today is the answer the engine gave at run time; no need to take our word for it. For failed arms, the record carries the failure reason — and where the engine's reply was retained, the full reply text and its SHA-256 hash stay in the export too, readable or not: the engine's actual words under hash, never a parsed verdict. Rows where the full text is absent carry an explicit full_text_retained: false marker, so a present hash without text never reads as tampering.
Retention: raw text is retained for 30 days; export before nulling to keep it. The hash survives the nulling. Visibility records are attributed, hashed instrument logs — a different evidence class from the sealed audit evidence above.
Reliability boundary
Processing is asynchronous and queue-backed. Completion time and uptime are not guaranteed. A completed audit verdict does not guarantee provider crawling, indexing, ranking, citation, training, recommendation, compliance, or understanding.