The AnswerSEO REST API gives Studio-plan accounts programmatic access to scans and Citation Readiness data. All endpoints are JSON in / JSON out, mounted under /api/v1, and authenticated with a Bearer token minted from your dashboard.
API access is gated on the Studio plan. Free and Pro accounts can view this reference but cannot mint tokens. See pricing for details.
Mint a token at /dashboard/account/tokens. The plaintext is shown once at creation; only its SHA-256 hash is stored. Pass it in the Authorization header on every request:
Authorization: Bearer asr_live_<your token>Tokens are full-account today (no per-token scopes yet). A downgrade from Studio revokes API access on the next request — existing tokens are not deleted, just rejected with 402 plan_required until the plan is restored.
POST /api/v1/scan — 10 requests per minute per token.GET /scan/:id, GET /projects/:id/readiness) — 120 requests per minute per token, shared bucket.Over-limit responses return HTTP 429 with a Retry-After header (seconds).
All errors return JSON of shape { error, message? }.
| HTTP | error | When |
|---|---|---|
| 401 | unauthorized | Missing or malformed Authorization header. |
| 401 | invalid_token | Token not recognised or revoked. |
| 402 | plan_required | Account is not on the Studio plan. |
| 402 | quota_exceeded | Monthly credit limit reached. |
| 402 | domain_limit | Plan's active-domain allowance reached. |
| 404 | not_found | Resource not found or not owned by your account (we don't leak existence across users). |
| 409 | scan_in_progress | The project already has a queued/running scan. |
| 409 | duplicate_domain | Another active project on the same account uses this domain. |
| 422 | invalid_body | Body failed schema validation. issues array is included. |
| 429 | rate_limited | Per-token rate limit exceeded. |
POST /api/v1/scanKick off a scan for one of your projects. The pipeline runs detached; poll the scan-status endpoint to track progress.
Body: { "projectId"?: string }. Omit projectId and the newest project on the account is used.
Success: 202 { "scanId": "...", "status": "queued" }.
curl -X POST https://answerseo.app/api/v1/scan \
-H "Authorization: Bearer asr_live_..." \
-H "Content-Type: application/json" \
-d '{"projectId":"clx..."}'GET /api/v1/scan/:idPoll scan status. Returns 404 if the scan does not exist or is not owned by the token's account.
curl https://answerseo.app/api/v1/scan/clx... \
-H "Authorization: Bearer asr_live_..."
{
"id": "clx...",
"projectId": "clx...",
"status": "running",
"step": 4,
"visibilityScore": null,
"startedAt": "2026-06-17T22:30:14.000Z",
"finishedAt": null,
"lastHeartbeatAt": "2026-06-17T22:33:02.000Z",
"error": null
}GET /api/v1/projects/:id/readinessReturns per-prompt Citation Readiness from the project's most recent completed scan. Rows are produced during the scan — no recompute on read. Returns 404 no_completed_scan if no completed scan exists yet.
Per-row signals (each 0–100):
answer — answer-first surface area.evidence — stats / quotes / authoritative outbound links.structure — main-content formatting density.fresh — recency of the page's modified date.retrieve — GSC rank + AI-crawler allow + off-page authority proxy.curl https://answerseo.app/api/v1/projects/clx.../readiness \
-H "Authorization: Bearer asr_live_..."
{
"projectId": "clx...",
"scanId": "clx...",
"scoredAt": "2026-06-17T22:42:01.000Z",
"composite": 71,
"rows": [{
"promptId": "clx...",
"promptText": "best AI dubbing for podcasters",
"url": "https://yoursite.com/dubbing",
"competitorUrl": "https://competitor.com/dubbing",
"competitorHost": "competitor.com",
"matchType": "page",
"you": { "readiness": 71, "signals": {"answer":80,"evidence":62,"structure":55,"fresh":90,"retrieve":70}, "freshDate": "2026-06-12" },
"them": { "readiness": 64, "signals": {"answer":70,"evidence":58,"structure":60,"fresh":40,"retrieve":80}, "freshDate": "2026-04-02" },
"gap": 7,
"fixes": ["Add an answer-first H2 within the first 30% of the page", "..."]
}]
}Register an https endpoint at /dashboard/account/webhooks to receive a signed POST whenever a scan completes or fails. Studio plan required.
scan.completed — fired after a scan reaches status: "done". Payload includes id, projectId, visibilityScore, finishedAt.scan.failed — fired when a scan terminates in status: "failed". Payload includes id, projectId, error, currentStep, finishedAt.Content-Type: application/jsonUser-Agent: AnswerSEO-Webhook/1X-Answerseo-Event: scan.completed (or scan.failed / webhook.test)X-Answerseo-Signature: t=<unix seconds>,v1=<hex>import crypto from "node:crypto"
const SECRET = process.env.ANSWERSEO_WEBHOOK_SECRET // whsec_...
export function verify(rawBody, sigHeader) {
const m = /^t=(\d+),v1=([0-9a-f]+)$/.exec(sigHeader ?? "")
if (!m) return false
const [, t, v1] = m
const mac = crypto.createHmac("sha256", SECRET)
.update(`${t}.${rawBody}`)
.digest("hex")
// Constant-time compare so timing leaks don't expose the secret.
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1))
// ±5-minute replay window.
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
return ok && fresh
}import hmac, hashlib, os, re, time
SECRET = os.environ["ANSWERSEO_WEBHOOK_SECRET"]
def verify(raw_body: bytes, sig_header: str) -> bool:
m = re.match(r"^t=(\d+),v1=([0-9a-f]+)$", sig_header or "")
if not m: return False
t, v1 = m.group(1), m.group(2)
mac = hmac.new(SECRET.encode(), f"{t}.{raw_body.decode()}".encode(),
hashlib.sha256).hexdigest()
if not hmac.compare_digest(mac, v1): return False
return abs(time.time() - int(t)) < 300We retry up to 3 times per delivery with exponential backoff (~0.5s / 1s / 2s). After 10 consecutive failures (5xx, timeouts, or unreachable hosts) the endpoint is auto-paused. Re-register or contact info@answerseo.app to reactivate.
The current version is v1. Breaking changes will ship under a new /api/v2 prefix; v1 stays supported for at least 12 months after a successor is announced.
Hit a wall? Email info@answerseo.app.