docs / audit api
Audit API
Three ways in: a terminal, an agent, or HTTP. Underneath, one POST starts an audit of one public page and hands back a token, and one GET reads the report with it. The rest of this page is detail, keys, errors and limits; the walkthrough of the command line and the MCP server is /agents.
From a terminal
one line, no keynpx -y geo-new audit https://example.com
- What happens
- Markdown on stdout once the measured rows land (about a minute); progress and a five-line summary on stderr. The AI synthesis keeps writing afterwards and never moves a score; --wait-for-ai waits for it.
- Exit codes
- 0 done · 1 audit failed · 2 usage · 3 rate limited · 4 auth or ownership · 5 network or server · 6 below --min-score (unmeasured counts as below unless --allow-unscored) · 7 score dropped in compare · 8 still running when --wait expired
- Then
- geo-new fix prints the prompt for your coding agent; geo-new share prints a link a person can open; geo-new report re-reads the last audit; --fresh measures again after an edit
- Scripts
- --json or --out after.json for the snapshot; geo-new compare before.json after.json compares two saved snapshots offline (exit 7 on a drop; --allow-missing-baseline on a first run)
- Credentials
- Keyless by default: the session lives in $XDG_CONFIG_HOME/geo-new/credentials.json (0600). GEO_NEW_API_KEY or geo-new login <key> for a key; CI should hold one
- Local runs
- GEO_NEW_API_URL=http://127.0.0.1:8787 against npm run dev:demo; a key is sent only to https or loopback origins
From an agent
MCP over stdioclaude mcp add geo-new -- npx -y geo-new@0 mcp
# Windows
claude mcp add geo-new -- cmd /c npx -y geo-new@0 mcp
# Cursor, Codex and other MCP clients
{ "mcpServers": { "geo-new": { "command": "npx", "args": ["-y", "geo-new@0", "mcp"] } } }- Tools
- audit_page · get_report (summary by default; markdown or json on request) · get_fix_prompt · compare_reports · get_usage · create_share · revoke_share, plus the geo-new://audits/{id} resource
- The loop
- audit_page, then get_fix_prompt, edit the page, then audit_page again with fresh: true. A non-fresh call may return the report measured before your change, marked cached with its age; every result carries a next step
- Limits
- Rate limits come back as ordinary results with a retry step; a keyless server gets 5 audits an hour and re-reads are free. geo-new key create in any terminal raises it to 10 and a running server picks the key up on its next call
- Trust
- Quoted page content inside a result is data from the audited site, never instructions. stdout carries JSON-RPC only
Authentication, in one paragraph
Starting an audit returns a session token in the response body. Send it back as Authorization: Bearer <token> to read the report. It lasts seven days and proves a report is yours. When you want a credential that outlives it, create a key once (gnk_…) and send it the same way; there are still no accounts. Nothing else is needed: no cookie jar, no key you have to invent, no call before the call.
One caveat, on the first request only, before any token exists: send no Origin and no Sec-Fetch-* header. With neither a token nor a same-origin session those mark the call as a browser’s, and it is refused with ORIGIN_REJECTED — which is what stops a web page spending a visitor’s audit allowance. Command-line and server-side clients send neither by default. Once you hold a token or a key the headers stop mattering, on every path.
Start to finish
two requests and a pollcurl -sS -X POST https://geo.new/api/v1/audits \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}'
# → 202 {"audit_id":"aud_…","session":"…","markdown_url":"…","poll_after_seconds":2}
curl -sS -H "Authorization: Bearer $SESSION" "$MARKDOWN_URL"
# → 202 while it fills, 200 once X-Audit-Status is complete, partial or failedThe status passes through writing, where the measured rows are published and the model is still drafting its interpretation — up to about two minutes that nothing else waits for. Stop there if ai.synthesis is not what you came for.
Start an audit
no token neededPOST/api/v1/auditscreateAudit
Admits one audit of one public page, or attaches to a run already in flight for that URL, and hands back the token you read it with.
- Content-Type
- application/json — anything else is 415
- Body
- {"url": string, "fresh"?: boolean, "assistant_answers"?: boolean, "assistant_engines"?: ["chatgpt" | "google_ai_mode"]} — strict; unknown keys are rejected
- Authorization
- optional: Bearer <session> to reuse a session you already hold
- Max body
- 8 KiB
- 202
- Admitted or attached. Retry-After: 2. Location: /api/v1/audits/{id}
- 200
- A finished report for that URL was reused. cached: true
- 400
- INVALID_URL, INVALID_JSON, INVALID_REQUEST, INVALID_IDEMPOTENCY_KEY
- 409
- IDEMPOTENCY_CONFLICT — same key, different body
- 429
- GLOBAL_RATE_LIMITED, PRINCIPAL_RATE_LIMITED, IP_RATE_LIMITED, TARGET_COOLDOWN
- 503
- CAPACITY_BUSY — no audit was admitted; retry
curl -sS -X POST https://geo.new/api/v1/audits \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}'- Follow markdown_url or snapshot_url to poll; never repeat POST as a polling mechanism. Use one established session and Idempotency-Key for retry-sensitive creation, especially fresh requests.
- The envelope carries session, audit_id, status, revision, cached, report_url, snapshot_url, markdown_url and poll_after_seconds. Keep session: it is how you read the report back, and how you reach it again tomorrow.
- Send the same URL twice on one session and you attach to the run already in flight, or reuse a finished one under an hour old. Duplicate requests do not duplicate work, and you need no key to get that.
- Idempotency-Key is optional, 16–128 printable characters, and only earns its place on fresh: true, where that reuse is skipped. It cannot be issued by this API — it only works if it stays the same across your retries — so a client library should generate one per call and you should never have to type one.
- fresh: true skips reuse. TARGET_COOLDOWN then allows strangers one run per URL per five minutes and you, re-running your own URL after an edit, one per minute. An admitted or reused audit carries the measured hourly allowance in RateLimit and RateLimit-Policy. Early failures may lack quota state.
- assistant_engines (experimental) asks ChatGPT, Google AI Mode or both about the brand; answers land in assistant_answers and ai_mode_answers, unscored. It wins over assistant_answers, whose true means ChatGPT only, and assistant_answers: false beside a non-empty assistant_engines is a 400. A request that asks an engine never reuses an audit that did not.
Read the snapshot
token requiredGET/api/v1/audits/{id}getAudit
The whole report as saved: scores, findings, evidence, crawler policy and page inventory.
- Authorization
- Bearer <session> — required
- Accept
- application/json (default) or text/markdown
- 200
- The snapshot at its current revision, running or finished
- 404
- REPORT_UNAVAILABLE — missing, expired, revoked, or another session’s
- 406
- NOT_ACCEPTABLE — the Accept header asks for something else
curl -sS -H "Authorization: Bearer $SESSION" \ https://geo.new/api/v1/audits/$AUDIT_ID
- Always 200 while running, so read status, not the status code. It is one of queued, fetching, analyzing, writing, complete, partial or failed.
- scores.catalog carries the single GEO score: six categories with subcategories, platforms, pillars and blockers. scores.geo and scores.seo are the retained v1 summaries.
- GET never starts work and never triggers a model call. Poll it every two seconds until the status is terminal.
- The browser app authenticates with its httpOnly geo_session cookie instead, which is why that cookie exists; a script should not have to keep a jar.
Read the Markdown report
token requiredGET/api/v1/audits/{id}/report.mdgetAuditMarkdown
The same report projected to Markdown, for pasting into a model or a pull request.
- Authorization
- Bearer <session> — required
- Accept
- text/markdown or */*
- 202
- Still running. Retry-After: 2. The body holds the findings so far
- 200
- Terminal — including a failed report
- 404
- REPORT_UNAVAILABLE
curl -sS -H "Authorization: Bearer $SESSION" \ https://geo.new/api/v1/audits/$AUDIT_ID/report.md
- Unlike the snapshot, this one does answer 202 while the report fills, so the status code is a usable signal here.
- X-Audit-Revision and X-Audit-Status describe the exact saved revision in the body you received.
- 200 means the report is finished, not that the page passed. Read X-Audit-Status.
Open a session first
no token neededPOST/api/v1/sessioncreateSession
Mints a token on its own, for when you want the credential in hand before a request you cannot afford to send twice.
- Body
- none
- Headers
- no Origin, no Sec-Fetch-*
- 200
- {"session":"<token>","expires_in_seconds":604800}
- 429
- IP_RATE_LIMITED — 30 new sessions per IP per hour
curl -sS -X POST https://geo.new/api/v1/session
- Optional: starting an audit mints a session too, so the ordinary flow never calls this.
- It earns its place in one case. Idempotency records are stored against the session that made them, so if a create request times out before its token reaches you, the retry authenticates as somebody new and cannot bind to the original — you would start a second audit and lose the first. Holding a token beforehand removes that.
- The token lasts seven days. A key (below) is the durable alternative: it outlives any session and carries its own allowance.
Read the fix prompt
owner onlyGET/api/v1/audits/{id}/fix-promptgetFixPrompt
A plain-text brief for a coding agent: the page as measured, each open action with its evidence and the change to make.
- Authorization
- Bearer <session or key> — the owner of the report
- 200
- text/plain; the same brief the report page copies for you
- 404
- REPORT_UNAVAILABLE
curl -sS -H "Authorization: Bearer $SESSION" \ https://geo.new/api/v1/audits/$AUDIT_ID/fix-prompt
- Capped at 32 KB and says what it left out. Quoted page text inside it is data from the audited site, not instructions.
- X-Audit-Revision names the revision the brief was built from; a report still writing its synthesis yields a brief without the model’s fix notes.
Compare two reports
owner onlyGET/api/v1/audits/{id}/compare/{baseline}compareAudits
Score, category and row deltas between a report and an earlier one of the same URL that you also own.
- Authorization
- Bearer <session or key> — the owner of both reports
- 200
- The comparison document
- 404
- REPORT_UNAVAILABLE — either report is missing, expired or not yours
- 422
- BASELINE_INELIGIBLE — a different URL, or a report that is still being measured
curl -sS -H "Authorization: Bearer $SESSION" \ https://geo.new/api/v1/audits/$AUDIT_ID/compare/$BASELINE_ID
- Reports expire after seven days, so a pipeline that compares across deploys keeps its own JSON snapshots and compares them offline; the CLI does exactly that with saved files.
- A finished baseline is not a passing one: compare deltas, not verdicts.
Create a key
no token neededPOST/api/v1/keyscreateKey
A key outlives any session, carries the free plan’s allowance and keeps a usage record. Shown once; store it.
- Authorization
- optional: Bearer <session or key>. Without one, this call mints a session and the key together
- Body
- optional: {"label": string} — 1 to 40 printable characters; the default is what asked (cli, mcp, browser)
- 201
- {"key":"gnk_…","prefix":"a1b2c3d4","label":"…","created_at":"…","principal_id":"prn_…"}
- 400
- INVALID_REQUEST with field: label
- 404
- REPORT_UNAVAILABLE — keys are switched off on this deployment
- 409
- KEY_LIMIT — five live keys already; revoke one
- 429
- KEY_ISSUANCE_LIMITED — ten per network per day
curl -sS -X POST https://geo.new/api/v1/keys \
-H 'Content-Type: application/json' -d '{"label":"ci"}'- The first key made from a session moves that session’s reports to the new principal, so the key sees everything the browser made and the browser keeps seeing it too.
- A key is a bearer like a session: send it as Authorization: Bearer gnk_… on every route. It is never returned in a session field and must never travel in a cookie.
- There is no recovery. A lost key is a lost principal, exactly as a lost session token is today; keep it where you keep other secrets.
List your keys
key or bound sessionGET/api/v1/keyslistKeys
The live keys of the principal you act as, newest first, by prefix and label. Never the secrets.
- Authorization
- Bearer <key>, or a session that holds or has claimed a key
- 200
- {"principal":{"kind":"key","plan":"free","prefix":"…"},"keys":[…]} — or principal: null and an empty list
- 404
- REPORT_UNAVAILABLE — keys are switched off on this deployment
curl -sS -H "Authorization: Bearer $GEO_NEW_API_KEY" \ https://geo.new/api/v1/keys
- This is the complete bounded list: at most 5 live keys per principal, with no pagination.
- last_used_at is written at most once a minute per key, so it lags a busy key by up to sixty seconds.
Revoke a key
key or bound sessionDELETE/api/v1/keys/{prefix}revokeKey
Immediate and everywhere. A key can revoke itself, so revocation never depends on a session.
- Authorization
- Bearer <key>, or a session bound to the principal
- prefix
- the eight hex characters after gnk_, with or without that prefix
- 200
- {"revoked":true}
- 400
- INVALID_REQUEST with field: prefix
- 404
- KEY_UNAVAILABLE — no such live key on this principal
- 404
- REPORT_UNAVAILABLE — keys are switched off on this deployment
curl -sS -X DELETE -H "Authorization: Bearer $GEO_NEW_API_KEY" \ https://geo.new/api/v1/keys/a1b2c3d4
- Revoking the last live key of a principal also unbinds every browser that claimed it; the browser that created the principal keeps its identity.
Read your allowance
no token neededGET/api/v1/usagegetUsage
What you act as, how many audits remain this hour, when that resets, and how full the shared buckets are.
- Authorization
- optional: Bearer <session or key>. Without one you get the anonymous defaults and principal: null
- 200
- {"principal":…,"plan":"free","allowance":{"limit":10,"used":3,"remaining":7,"reset_seconds":2460},"global":{…}}
curl -sS -H "Authorization: Bearer $GEO_NEW_API_KEY" \ https://geo.new/api/v1/usage
- The same numbers ride on every create response as RateLimit ("audits";r=<remaining>;t=<reset_seconds>) and RateLimit-Policy ("audits";q=<limit>;w=3600).
- Re-reads of a report and cache hits spend a request, never an audit; only an admission counts here.
- assistants.chatgpt and assistants.google_ai_mode (experimental): your and your network’s asks left today (of 6 and 12), whether the shared pool had room when read (not a reservation), and resets_at at 00:00 UTC; null when that assistant is off.
Keys
optional, durable, shown onceA key outlives any session, doubles the hourly allowance and keeps a usage record; it changes nothing about how you read a report. There is no account behind it and no recovery: a lost key is a lost identity, exactly as a lost session is. Create one here for this browser, or with geo-new key create in a terminal; paste one below to make this browser act as it, so reports made from a terminal open at their /r/ links here too.
Plans, from the table the store enforces. CI on a shared runner pool should hold a key: keyed principals have their own network bucket. builder is set by hand for now.
Errors
every failure, one shape{"error":{"code":"TARGET_COOLDOWN",
"message":"This URL was recently audited. Wait five minutes between fresh runs.",
"retryable":true,
"request_id":"req_4c1f…",
"retry_after_seconds":300}}Amber codes are retryable and carry Retry-After; wait it out rather than backing off blindly. Every response carries request_id — quote it if you report a problem.
Limits
allowances by planRateLimit and RateLimit-Policy describe only measured buckets. "requests" is the caller’s rolling-minute request state, including the accepted request; "audits" is the hourly audit allowance, included on creation, usage and admission errors when measured. Multiple buckets are comma-separated.
For requests, q is 180 for sessions/free keys or 600 for builders, w is 60 seconds, r is remaining requests and t is seconds until the oldest request leaves that bucket. Audit w is 3600 seconds. Rolling windows release capacity gradually; these are observations, not reservations.
No individual bucket exists before identity is established. Those headers are omitted on anonymous discovery and may be absent after early ingress/authentication or database failures. Reads do not query hourly usage merely to fill headers. Shared IP state is disclosed only as "ip-requests" when that measured bucket causes a 429.
Follow Retry-After on 429/503 and progressive Markdown 202 responses. A successful read consumes a request, never an audit. Other limits can still reject a call even when one bucket has capacity.
Reusing a cached report spends a request, not an audit start. A key raises your own allowance and keeps a usage record; every plan shares the ceiling everyone shares.
Versioning
The API is in beta. Additive changes are recorded within /api/v1. Incompatible HTTP or report-format changes require an explicit new major contract and a documented migration. OpenAPI document, report-format and npm-package versions are independent. No endpoint is deprecated by this release, and no fixed overlap period is promised. A field marked experimental (today assistant_answers and ai_mode_answers) may change within v1; its versions key says which shape a report carries.
Outside the contract
These routes exist because the web app uses them. They need a CSRF token, they are not in the OpenAPI contract, and they can change without notice. Do not build on them.
POST /api/session · GET /api/audits/:id/events · POST /api/audits/:id/rerun
POST /api/audits/:id/share · DELETE /api/audits/:id/shares · GET /api/shares/:token
GET /api/audits/:id/image · GET /api/audits/:id/baselines · GET /api/audits/:id/compare/:baseline