{"openapi":"3.1.0","info":{"title":"SERP Agent API","version":"1.0.0","description":"Agent-ready SEO automation API: programmatic signup, technical site audits, and end-to-end SEO article generation. Designed for AI agents — instant sandbox keys (no CAPTCHA, no payment method), uniform machine-readable errors, idempotent POSTs, and signed webhooks. All request and response fields are snake_case. Quickstart for agents: https://serp-agent.com/docs/agents","contact":{"url":"https://serp-agent.com/docs"}},"servers":[{"url":"https://serp-agent.com","description":"Production"}],"security":[{"apiKey":[]}],"tags":[{"name":"Accounts","description":"Programmatic signup and account status."},{"name":"API keys","description":"Create, list, and revoke API keys (max 5 active per account)."},{"name":"Projects","description":"Sites/domains the API works on. Sandbox keys: 1 project."},{"name":"Audits","description":"Technical SEO audit runs (async: enqueue, then poll)."},{"name":"Articles","description":"Single-article SEO content generation (async: enqueue, then poll)."},{"name":"Plans","description":"Public machine-readable pricing."},{"name":"Billing","description":"Per-project subscriptions and prepaid credits (Creem checkout — a human completes payment in the browser)."},{"name":"Webhooks","description":"Outbound event notifications with HMAC signatures."}],"paths":{"/v1/accounts":{"post":{"operationId":"createAccount","summary":"Create an account (programmatic signup)","description":"Public, CAPTCHA-free signup. Returns a sandbox API key **immediately** — shown exactly once, before any email verification. Sandbox keys expire after 14 days and carry hard usage caps (1 project, 1 audit, 2 articles). Rate limited to 5 signups/hour per IP. Idempotency-Key is scoped to the caller's IP so a retried signup replays the stored 201 (raw key included) instead of hitting `account_exists`.","tags":["Accounts"],"security":[],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"x-sandbox-limits":{"audits":1,"articles":2,"projects":1,"expires_days":14},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":320,"description":"Trimmed and lowercased server-side."},"company_name":{"type":"string","minLength":1,"maxLength":200},"origin":{"type":"string","enum":["agent","human"],"description":"Who is signing up. Defaults to \"agent\"."}}},"example":{"email":"agent@example.com","company_name":"Acme Agents","origin":"agent"}}}},"responses":{"201":{"description":"Account created. `api_key` is the raw sandbox key — it is shown exactly once and never retrievable again.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"},"example":{"account_id":"usr_01h9x2","api_key":"sk_sandbox_REDACTED_EXAMPLE_KEY","mode":"sandbox","sandbox_limits":{"audits":1,"articles":2,"expires_days":14},"upgrade_url":"https://serp-agent.com/pricing","docs":"https://serp-agent.com/docs/agents"}}}},"400":{"description":"Malformed JSON or invalid field. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Invalid email: Invalid email address.","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"409":{"description":"Email already registered. Code: `account_exists`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"account_exists","message":"An account with this email already exists. Log in to the dashboard and manage API keys at /settings, or authenticate with an existing API key.","doc_url":"https://serp-agent.com/docs/errors#account_exists"}}}}},"429":{"description":"Too many signups from this IP (5/hour). Code: `rate_limited`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many signup attempts from this IP. Try again later.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}},"500":{"description":"Unexpected server error — safe to retry with the same Idempotency-Key. Code: `internal_error`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"internal_error","message":"Something went wrong creating the account. Try again.","doc_url":"https://serp-agent.com/docs/errors#internal_error"}}}}}}}},"/v1/account":{"get":{"operationId":"getAccount","summary":"Get the calling account","description":"Snapshot of the account behind the presented API key. Sandbox callers additionally see their usage ledger (`sandbox_usage`) and limits so an agent can self-manage the trial budget. `plan` is currently always `null` (populated by the upcoming billing phase).","tags":["Accounts"],"responses":{"200":{"description":"The account snapshot.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"account_id":"usr_01h9x2","email":"agent@example.com","email_verified":false,"mode":"sandbox","plan":null,"sandbox_usage":{"audits":1,"articles":0},"sandbox_limits":{"audits":1,"articles":2,"expires_days":14},"created_at":"2026-07-23T09:00:00.000Z","projects_count":1}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/api-keys":{"post":{"operationId":"createApiKey","summary":"Create an additional API key","description":"Mints a key of the **same mode** as the calling key (sandbox keys mint sandbox keys, live mint live) — a trial can never self-upgrade. Sandbox rotations inherit the calling key's expiry and usage ledger, so minting keys never extends or resets the trial budget. Max 5 active keys per account. The raw key appears only in this response. Empty body is valid (`name` is optional).","tags":["API keys"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100}}},"example":{"name":"ci-runner"}}}},"responses":{"201":{"description":"Key created. `api_key` is the full raw key — shown exactly once.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"},"example":{"id":"ck_key_01h9x3","name":"ci-runner","key_prefix":"sk_sandbox_","last4":"9fQz","mode":"sandbox","created_at":"2026-07-23T09:00:00.000Z","last_used_at":null,"expires_at":"2026-08-06T09:00:00.000Z","revoked_at":null,"api_key":"sk_sandbox_REDACTED_EXAMPLE_KEY"}}}},"400":{"description":"Malformed body (`invalid_request`) or 5 active keys already exist (`key_limit_reached`).","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"key_limit_reached","message":"You already have 5 active API keys. Revoke one (DELETE /v1/api-keys/{id}) before creating another.","doc_url":"https://serp-agent.com/docs/errors#key_limit_reached"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}},"get":{"operationId":"listApiKeys","summary":"List API keys","description":"All keys on the account, newest first. Only `key_prefix` + `last4` are exposed — the full secret and hash never appear after creation.","tags":["API keys"],"responses":{"200":{"description":"The account's keys.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"},"example":{"keys":[{"id":"ck_key_01h9x2","name":"default","key_prefix":"sk_sandbox_","last4":"9fQz","mode":"sandbox","created_at":"2026-07-23T09:00:00.000Z","last_used_at":"2026-07-23T09:05:12.000Z","expires_at":"2026-08-06T09:00:00.000Z","revoked_at":null}]}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/api-keys/{id}":{"delete":{"operationId":"revokeApiKey","summary":"Revoke an API key","description":"A key may revoke itself, but revoking the FINAL active key is refused (`cannot_revoke_last_key`) — with zero active keys the account would be locked out. Rotation order: create the new key first, then revoke the old one. Revoking an already-revoked key is idempotent (returns the same terminal state).","tags":["API keys"],"parameters":[{"name":"id","in":"path","required":true,"description":"API key id (from POST/GET /v1/api-keys).","schema":{"type":"string"}}],"responses":{"200":{"description":"Key revoked (or already revoked).","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyEnvelope"},"example":{"key":{"id":"ck_key_01h9x2","name":"default","key_prefix":"sk_sandbox_","last4":"9fQz","mode":"sandbox","created_at":"2026-07-23T09:00:00.000Z","last_used_at":"2026-07-23T09:05:12.000Z","expires_at":"2026-08-06T09:00:00.000Z","revoked_at":"2026-07-23T10:00:00.000Z"}}}}},"400":{"description":"This is the only active key. Code: `cannot_revoke_last_key`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"cannot_revoke_last_key","message":"This is your only active API key — revoking it would lock the account out of the API. Create a replacement first (POST /v1/api-keys), then revoke this one.","doc_url":"https://serp-agent.com/docs/errors#cannot_revoke_last_key"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"404":{"description":"No such key on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such API key.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/projects":{"post":{"operationId":"createProject","summary":"Create a project","description":"Registers a site/domain to work on. Sandbox keys are capped at **1 project** and their projects carry `sandbox: true` (audits/articles run free within the trial budget; articles are draft-only). Live keys create projects freely — audits and articles are then gated per project by an active subscription, surfaced as `access: \"pending_payment\"` until payment completes. `name` defaults to the URL's hostname; `language` defaults to \"en\", `country` to \"US\".","tags":["Projects"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"x-sandbox-limits":{"projects":1},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","minLength":1,"maxLength":500,"description":"The site to work on. Normalized server-side; must be a valid http(s) URL."},"name":{"type":"string","minLength":1,"maxLength":120},"language":{"type":"string","minLength":2,"maxLength":10,"description":"Primary content language (lowercased). Default \"en\"."},"country":{"type":"string","minLength":2,"maxLength":10,"description":"Primary target country (uppercased). Default \"US\"."}}},"example":{"url":"https://example.com","name":"Example","language":"en","country":"US"}}}},"responses":{"201":{"description":"Project created.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"},"example":{"project_id":"proj_01h9x2","url":"https://example.com","name":"example.com","status":"setup","language":"en","country":"US","sandbox":true,"access":"sandbox","created_at":"2026-07-23T09:01:00.000Z"}}}},"400":{"description":"Malformed body or invalid URL. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"url must be a valid http(s) URL.","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"403":{"description":"Sandbox keys are limited to 1 project. Code: `sandbox_limit_reached`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"sandbox_limit_reached","message":"Sandbox keys are limited to 1 project. Upgrade to a paid plan to create more: https://serp-agent.com/pricing","doc_url":"https://serp-agent.com/pricing"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}},"get":{"operationId":"listProjects","summary":"List projects","description":"All projects on the account, newest first, each with keyword/article/audit counts.","tags":["Projects"],"responses":{"200":{"description":"The account's projects.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectList"},"example":{"projects":[{"project_id":"proj_01h9x2","url":"https://example.com","name":"example.com","status":"setup","language":"en","country":"US","sandbox":true,"access":"sandbox","created_at":"2026-07-23T09:01:00.000Z","counts":{"keywords":12,"articles":1,"audits":1}}]}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/projects/{projectId}":{"get":{"operationId":"getProject","summary":"Get a project","description":"Project details including counts and the latest completed technical-audit summary (`latest_audit`, null when no audit has completed yet).","tags":["Projects"],"parameters":[{"name":"projectId","in":"path","required":true,"description":"Project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Project details.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDetail"},"example":{"project_id":"proj_01h9x2","url":"https://example.com","name":"example.com","status":"running","language":"en","country":"US","sandbox":true,"access":"sandbox","created_at":"2026-07-23T09:01:00.000Z","counts":{"keywords":12,"articles":1,"audits":1},"latest_audit":{"audit_id":"aud_01h9x4","status":"completed","completed_at":"2026-07-23T09:30:00.000Z","score":74,"summary":{"issues_critical":2,"issues_warning":7,"issues_info":12,"top_issues":[{"check":"SSL","severity":"critical","title":"Certificate chain incomplete","affected_count":1},{"check":"PAGESPEED","severity":"warning","title":"Largest Contentful Paint above 2.5s on mobile","affected_count":4}],"categories":{"performance":68,"security":45,"indexability":90,"mobile":82,"structure":88}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"404":{"description":"No such project on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Project not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/projects/{projectId}/audits":{"post":{"operationId":"runAudit","summary":"Start a technical audit","description":"Enqueues an asynchronous technical SEO audit (a site crawl runs first when no usable crawl exists). Returns 202 with an `audit_id` to poll at GET /v1/audits/{audit_id}. If an audit is already queued/running for the project, that run is returned as-is (202, no duplicate job, no budget burn). Sandbox trial: 1 audit per key. Non-sandbox projects: included with an active subscription; without one, LIVE keys pay per unit from the prepaid credit balance (technical_audit = 25 credits — 402 `insufficient_credits` when the balance is short). Typical completion: a few minutes; poll every 30–60s.","tags":["Audits"],"parameters":[{"name":"projectId","in":"path","required":true,"description":"Project id.","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"x-sandbox-limits":{"audits":1},"responses":{"202":{"description":"Audit queued (or an already-active run returned). Poll the `poll` path.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditAccepted"},"example":{"audit_id":"aud_01h9x4","status":"queued","poll":"/v1/audits/aud_01h9x4"}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"402":{"description":"Payment needed. `payment_required` (sandbox key, no subscription) or `insufficient_credits` (live key, no subscription, balance below 25 credits — body carries `required_credits`, `balance_credits`, `purchase`, `subscription_alternative`).","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"insufficient_credits","message":"Not enough credits: technical_audit costs 25 credits, balance is 0. Buy a credit pack (POST /v1/credits/purchases) or subscribe the project (POST /v1/subscriptions — all-inclusive).","doc_url":"https://serp-agent.com/docs/errors#insufficient_credits"}}}}},"403":{"description":"Sandbox audit budget exhausted (1 per trial). Code: `sandbox_limit_reached`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"sandbox_limit_reached","message":"Sandbox audit budget exhausted (1 per trial). Upgrade to a paid plan to continue: https://serp-agent.com/pricing","doc_url":"https://serp-agent.com/pricing"}}}}},"404":{"description":"No such project on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Project not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/audits/{auditId}":{"get":{"operationId":"getAudit","summary":"Poll an audit run","description":"Status of a technical audit. `status` walks queued → running → completed (or failed). Completed audits carry `score` (0–100) and a SUMMARIZED report: severity counts, top issues grouped per check (capped at 10, worst first), and per-category scores — never a raw page-by-page dump. Failed audits carry `error`.","tags":["Audits"],"parameters":[{"name":"auditId","in":"path","required":true,"description":"Audit id from POST /v1/projects/{projectId}/audits.","schema":{"type":"string"}}],"responses":{"200":{"description":"Current audit state.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Audit"},"example":{"audit_id":"aud_01h9x4","project_id":"proj_01h9x2","status":"completed","started_at":"2026-07-23T09:20:00.000Z","completed_at":"2026-07-23T09:30:00.000Z","score":74,"summary":{"issues_critical":2,"issues_warning":7,"issues_info":12,"top_issues":[{"check":"SSL","severity":"critical","title":"Certificate chain incomplete","affected_count":1},{"check":"PAGESPEED","severity":"warning","title":"Largest Contentful Paint above 2.5s on mobile","affected_count":4}],"categories":{"performance":68,"security":45,"indexability":90,"mobile":82,"structure":88}}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"404":{"description":"No such audit on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Audit not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/projects/{projectId}/articles":{"post":{"operationId":"generateArticle","summary":"Generate one SEO article","description":"Enqueues end-to-end generation of ONE article for a topic and/or primary keyword (at least one required; the other is derived). Returns 202 with an `article_id` to poll at GET /v1/articles/{article_id}. The article is research-grounded, QA-checked, and returned as draft markdown — it is NEVER auto-published (fetch the content and publish it yourself). Only one API article may generate per project at a time (409 `article_in_progress`). Sandbox trial: 2 articles per key, watermarked as draft-only. Non-sandbox projects: included with an active subscription; without one, LIVE keys pay per unit from the prepaid credit balance (article_generated = 15 credits — 402 `insufficient_credits` when the balance is short; credit-funded articles stay draft-only). Typical completion: 10–30 minutes; poll every 60s.","tags":["Articles"],"parameters":[{"name":"projectId","in":"path","required":true,"description":"Project id.","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"x-sandbox-limits":{"articles":2},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"At least one of topic / primary_keyword is required.","properties":{"topic":{"type":"string","minLength":3,"maxLength":180,"description":"Working title / topic. Placed verbatim as the article's planned title."},"primary_keyword":{"type":"string","minLength":2,"maxLength":120,"description":"Target keyword. Derived from topic (lowercased) when omitted."}}},"example":{"topic":"How to migrate a WordPress site without losing rankings","primary_keyword":"wordpress migration seo"}}}},"responses":{"202":{"description":"Article generation queued. Poll the `poll` path.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleAccepted"},"example":{"article_id":"art_01h9x5","status":"queued","poll":"/v1/articles/art_01h9x5"}}}},"400":{"description":"Malformed body, or neither topic nor primary_keyword given. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Provide at least one of: topic, primary_keyword.","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"402":{"description":"Payment needed. `payment_required` (sandbox key, no subscription) or `insufficient_credits` (live key, no subscription, balance below 15 credits — body carries `required_credits`, `balance_credits`, `purchase`, `subscription_alternative`).","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"insufficient_credits","message":"Not enough credits: article_generated costs 15 credits, balance is 0. Buy a credit pack (POST /v1/credits/purchases) or subscribe the project (POST /v1/subscriptions — all-inclusive).","doc_url":"https://serp-agent.com/docs/errors#insufficient_credits"}}}}},"403":{"description":"Sandbox article budget exhausted (2 per trial). Code: `sandbox_limit_reached`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"sandbox_limit_reached","message":"Sandbox article budget exhausted (2 per trial). Upgrade to a paid plan to continue: https://serp-agent.com/pricing","doc_url":"https://serp-agent.com/pricing"}}}}},"404":{"description":"No such project on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Project not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"409":{"description":"An article is already generating for this project. Code: `article_in_progress`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"article_in_progress","message":"An article is already generating for this project. Poll /v1/articles/{article_id} and retry once it finishes.","doc_url":"https://serp-agent.com/docs/errors#article_in_progress"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/articles/{articleId}":{"get":{"operationId":"getArticle","summary":"Poll an article generation","description":"Status of an article. `status` walks generating → qa → ready (or failed); `published` appears when the operator publishes it later. Content fields (`title`, `meta_description`, `content_markdown`, `word_count`, `qa_score`, `language`) appear only once the article is ready or published — while generating you get status only. Sandbox articles carry a `watermark` object and are never published automatically.","tags":["Articles"],"parameters":[{"name":"articleId","in":"path","required":true,"description":"Article id from POST /v1/projects/{projectId}/articles.","schema":{"type":"string"}}],"responses":{"200":{"description":"Current article state.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"},"example":{"article_id":"art_01h9x5","project_id":"proj_01h9x2","status":"ready","title":"How to Migrate a WordPress Site Without Losing Rankings","meta_description":"A step-by-step WordPress migration checklist that protects your search rankings…","content_markdown":"## Why migrations lose rankings\n\n…","word_count":1840,"qa_score":92,"language":"en","watermark":{"sandbox":true,"note":"draft-only sandbox article"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"404":{"description":"No such article on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Article not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/plans":{"get":{"operationId":"getPlans","summary":"Get pricing (public)","description":"Canonical machine-readable pricing. No auth. Lists the human subscription plans (basic $199 / pro $299 per project per month, Creem as merchant of record), the agent channel (per-project programmatic purchase of the same plans via `POST /v1/subscriptions`), an a-la-carte unit catalog with prepaid credit packs (`POST /v1/credits/purchases`, balance at `GET /v1/credits`), and the free sandbox trial. Cached 5 minutes.","tags":["Plans"],"security":[],"responses":{"200":{"description":"The pricing document. (Response example abridged — fetch the endpoint for the full catalog.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlansDocument"},"example":{"currency":"USD","updated_at":"2026-07-23T00:00:00Z","merchant_of_record":"Creem (taxes/VAT calculated at checkout by location)","plans":[{"id":"basic","name":"Basic","audience":"human","monthly_price":199,"currency":"USD","period":"month","featured":false,"features":["1 project","Weekly audit","…"],"checkout_url":"https://serp-agent.com/pricing"},{"id":"pro","name":"Pro","audience":"human","monthly_price":299,"currency":"USD","period":"month","featured":true,"features":["1 project · daily audit","…"],"checkout_url":"https://serp-agent.com/pricing"}],"agent_channel":{"audience":"agent","model":"per_project","note":"The same plans are purchasable programmatically, one subscription per project (per site/domain). No base platform fee — you pay only for the plans or units you buy.","signup":"POST /v1/accounts","subscriptions":"POST /v1/subscriptions {project_id, plan_id: \"basic\"|\"pro\"} → checkout_url (a human completes the Creem checkout; the project activates automatically on payment)","subscription_status":"GET /v1/subscription?project_id=...","base_monthly_fee":0,"docs":"https://serp-agent.com/for-agents"},"a_la_carte":{"note":"Buy individual SEO work units without a subscription. 1 credit = $1 list price.","payment":"prepaid credit packs via POST /v1/credits/purchases (Creem checkout); balance + unit prices at GET /v1/credits","credit_packs":[{"id":"pack_500","usd":500,"credits":500,"bonus_credits":0},{"id":"pack_2000","usd":2000,"credits":2200,"bonus_credits":200},{"id":"pack_10000","usd":10000,"credits":12500,"bonus_credits":2500}],"currency":"USD","items":[{"id":"article_generated","unit":"article","usd_price":15,"description":"One SEO article generated end-to-end…"}]},"sandbox":{"note":"Free trial for agents — instant API key, no payment method, no CAPTCHA.","audits":1,"articles":2,"expires_days":14,"price":0,"signup":"POST /v1/accounts"},"links":{"pricing_page":"https://serp-agent.com/pricing","for_agents":"https://serp-agent.com/for-agents","llms_txt":"https://serp-agent.com/llms.txt"}}}}}}}},"/v1/subscriptions":{"post":{"operationId":"createSubscription","summary":"Buy a per-project subscription","description":"Creates a Creem checkout for a monthly subscription on ONE project — the same basic ($199) / pro ($299) products the dashboard sells. Returns `checkout_url`: surface it to your human, who completes payment in the browser (Creem is merchant of record; taxes at checkout). The project activates automatically via webhook on payment — poll GET /v1/subscription?project_id=... . Subscribed projects run audits/articles all-inclusive (no credit charges). Sandbox keys get 403 `sandbox_not_billable` (the trial is free by design). An already actively-subscribed project returns 409 `already_subscribed`.","tags":["Billing"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["project_id","plan_id"],"properties":{"project_id":{"type":"string"},"plan_id":{"type":"string","enum":["basic","pro"],"description":"basic = $199/mo, pro = $299/mo (see GET /v1/plans)."}}},"example":{"project_id":"proj_01h9x2","plan_id":"pro"}}}},"responses":{"201":{"description":"Checkout created — hand `checkout_url` to a human to complete payment.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCheckout"},"example":{"status":"pending_payment","checkout_url":"https://checkout.creem.io/...","project_id":"proj_01h9x2","plan":"pro","note":"Open checkout_url in a browser to complete payment (a human completes the Creem checkout). The project activates automatically on payment — poll GET /v1/subscription?project_id=proj_01h9x2 for status."}}}},"400":{"description":"Malformed body or unknown plan_id. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Invalid request: plan_id Invalid enum value. Expected {project_id, plan_id: \"basic\"|\"pro\"}.","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"403":{"description":"Sandbox keys cannot make purchases. Code: `sandbox_not_billable`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"sandbox_not_billable","message":"Sandbox keys cannot make purchases — the sandbox trial is free by design. Purchases require a live key (sk_live_).","doc_url":"https://serp-agent.com/docs/errors#sandbox_not_billable"}}}}},"404":{"description":"No such project on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Project not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"409":{"description":"The project already has an active subscription. Code: `already_subscribed`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"already_subscribed","message":"This project already has an active subscription (paid through 2026-08-23). Check GET /v1/subscription?project_id=proj_01h9x2.","doc_url":"https://serp-agent.com/docs/errors#already_subscribed"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}},"503":{"description":"Billing for this plan is not configured on the server yet. Code: `billing_not_configured`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"billing_not_configured","message":"Subscription checkout for plan \"pro\" is not configured on this server yet (operator has not set the Creem product). Try again later or contact support.","doc_url":"https://serp-agent.com/docs/errors#billing_not_configured"}}}}}}}},"/v1/subscription":{"get":{"operationId":"getSubscription","summary":"Get a project's subscription state","description":"Billing state of one project, derived from the same fields the access gate reads. `status`: `active` (live paid month — work endpoints are all-inclusive), `pending_payment` (an API-created checkout is in flight, <24h old), or `none`. `plan` is `basic`/`pro` when resolvable (manual-tier marker or live Creem lookup), else null. Sandbox projects always report `none` (they run on the free trial budget).","tags":["Billing"],"parameters":[{"name":"project_id","in":"query","required":true,"description":"Project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The project's subscription state.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionStatus"},"example":{"project_id":"proj_01h9x2","plan":"pro","status":"active","paid_until":"2026-08-23T09:00:00.000Z"}}}},"400":{"description":"Missing project_id. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Query parameter project_id is required.","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"404":{"description":"No such project on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Project not found.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/credits":{"get":{"operationId":"getCredits","summary":"Get credit balance, unit prices, and packs","description":"Prepaid credit balance (1 credit = $1 list price) plus the unit price list and purchasable packs. Credits pay for individual work units on live keys when a project has NO subscription (subscribed projects are all-inclusive; sandbox keys use the free trial budget). Larger packs carry bonus credits (volume discount). `available: false` on a pack means the operator has not configured its product yet (purchase would 503).","tags":["Billing"],"responses":{"200":{"description":"Balance + catalog.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsOverview"},"example":{"balance_credits":460,"currency":"USD","credit_usd_value":1,"unit_prices":{"technical_audit":25,"article_generated":15,"article_published":5,"article_language_version":8,"deep_audit":99,"seo_strategy":75,"keyword_research_content_plan":50},"packs":[{"id":"pack_500","usd":500,"credits":500,"bonus_credits":0,"available":true},{"id":"pack_2000","usd":2000,"credits":2200,"bonus_credits":200,"available":true},{"id":"pack_10000","usd":10000,"credits":12500,"bonus_credits":2500,"available":true}],"purchase":"POST /v1/credits/purchases","notes":["1 credit = $1 list price. Larger packs include bonus credits (volume discount).","article_published (+5) is not auto-charged in v1: credit-funded articles are draft-only; publishing requires a project subscription."]}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/credits/purchases":{"post":{"operationId":"purchaseCredits","summary":"Buy a prepaid credit pack","description":"Creates a one-time Creem checkout for a credit pack. Returns `checkout_url`: surface it to your human, who completes payment in the browser. Credits land on the account balance automatically via webhook on payment — poll GET /v1/credits. Sandbox keys get 403 `sandbox_not_billable`. Packs whose Creem product is not configured yet return 503 `billing_not_configured`.","tags":["Billing"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (max 200 chars). Retrying a request with the same key replays the stored response verbatim (with header Idempotent-Replayed: true) instead of re-executing — including one-time secrets in the original body. Keys are scoped per account (per client IP on the unauthenticated signup) and pruned after 24h. Reusing a key on a different endpoint returns 409 idempotency_conflict.","schema":{"type":"string","maxLength":200},"example":"b3f1c9e2-1a77-4d0e-9f5a-2f4f4f4f4f4f"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["pack_id"],"properties":{"pack_id":{"type":"string","enum":["pack_500","pack_2000","pack_10000"]}}},"example":{"pack_id":"pack_2000"}}}},"responses":{"201":{"description":"Checkout created — hand `checkout_url` to a human to complete payment.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditPackCheckout"},"example":{"status":"pending_payment","checkout_url":"https://checkout.creem.io/...","pack_id":"pack_2000","usd":2000,"credits":2200,"bonus_credits":200,"note":"Open checkout_url in a browser to complete payment (a human completes the Creem checkout). Credits land automatically on payment — poll GET /v1/credits for the balance."}}}},"400":{"description":"Missing or unknown pack_id. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Unknown pack_id \"pack_1\". Available packs: pack_500, pack_2000, pack_10000 (see GET /v1/credits).","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"403":{"description":"Sandbox keys cannot make purchases. Code: `sandbox_not_billable`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"sandbox_not_billable","message":"Sandbox keys cannot make purchases — the sandbox trial is free by design. Purchases require a live key (sk_live_).","doc_url":"https://serp-agent.com/docs/errors#sandbox_not_billable"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}},"503":{"description":"This pack's Creem product is not configured yet. Code: `billing_not_configured`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"billing_not_configured","message":"Credit pack \"pack_2000\" is not purchasable yet on this server — the operator has not created its Creem product. Try again later, or subscribe the project instead (POST /v1/subscriptions).","doc_url":"https://serp-agent.com/docs/errors#billing_not_configured"}}}}}}}},"/v1/webhooks":{"post":{"operationId":"createWebhook","summary":"Register a webhook endpoint","description":"Registers an HTTPS endpoint to receive event POSTs. `events` filters which events are delivered — omit it (or pass `[]`) to subscribe to ALL events. The response includes the raw signing `secret` exactly once (only an encrypted form is stored). URLs must be public HTTPS (private/internal hosts are refused). Max 20 endpoints per account. Deliveries retry with exponential backoff (up to 8 attempts, capped at 6h between attempts); an endpoint is auto-deactivated after 50 consecutive failures.","tags":["Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Public HTTPS URL to POST events to."},"events":{"type":"array","items":{"type":"string","enum":["audit.completed","article.generated","article.published","subscription.updated","usage.threshold_reached","credits.updated"]},"description":"Events to deliver. Omit or [] = all events."}}},"example":{"url":"https://agent.example.com/hooks/serp-agent","events":["audit.completed","article.published"]}}}},"responses":{"201":{"description":"Endpoint registered. `secret` is shown exactly once — store it to verify signatures.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"},"example":{"webhook_id":"wh_01h9x6","url":"https://agent.example.com/hooks/serp-agent","events":["audit.completed","article.published"],"active":true,"created_at":"2026-07-23T09:10:00.000Z","secret":"3f9c…64-hex-chars…e1a0","signature_header":"X-SerpAgent-Signature","signature_scheme":"hex sha256 HMAC of the raw request body"}}}},"400":{"description":"Invalid URL (must be public https), unknown event name, or malformed body. Code: `invalid_request`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Webhook URLs must use https.","doc_url":"https://serp-agent.com/docs/errors#invalid_request"}}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"409":{"description":"Endpoint cap reached (20 per account). Code: `limit_reached`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"limit_reached","message":"At most 20 webhook endpoints per account. Delete one first (DELETE /v1/webhooks/{id}).","doc_url":"https://serp-agent.com/docs/errors#limit_reached"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}},"500":{"description":"Server-side secret encryption unavailable. Code: `internal_error`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"internal_error","message":"Secret encryption is not configured on the server.","doc_url":"https://serp-agent.com/docs/errors#internal_error"}}}}}}},"get":{"operationId":"listWebhooks","summary":"List webhook endpoints","description":"All endpoints on the account, oldest first. The signing secret is masked (shown only at creation).","tags":["Webhooks"],"responses":{"200":{"description":"The account's endpoints.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"},"example":{"webhooks":[{"webhook_id":"wh_01h9x6","url":"https://agent.example.com/hooks/serp-agent","events":["audit.completed","article.published"],"active":true,"secret":"whsec_************ (shown once at creation)","failure_count":0,"last_success_at":"2026-07-23T09:31:00.000Z","last_failure_at":null,"created_at":"2026-07-23T09:10:00.000Z"}]}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}},"/v1/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","summary":"Delete a webhook endpoint","description":"Removes the endpoint and its queued deliveries.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Endpoint deleted.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeleted"},"example":{"deleted":true,"webhook_id":"wh_01h9x6"}}}},"401":{"description":"Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sk_...","doc_url":"https://serp-agent.com/docs/errors#unauthorized"}}}}},"404":{"description":"No such endpoint on this account. Code: `not_found`.","headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No webhook endpoint with that id on this account.","doc_url":"https://serp-agent.com/docs/errors#not_found"}}}}},"429":{"description":"Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded for this API key.","doc_url":"https://serp-agent.com/docs/errors#rate_limited"}}}}}}}}},"webhooks":{"audit.completed":{"post":{"operationId":"webhookAuditCompleted","summary":"An audit run for one of your projects finished","description":"Sent when a project's audit run completes (all jobs finished). Verify X-SerpAgent-Signature (hex sha256 HMAC of the raw body with your endpoint secret) before trusting the payload.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"whd_01h9x7","event":"audit.completed","created_at":"2026-07-23T09:30:05.000Z","data":{"project_id":"proj_01h9x2","project_name":"example.com","status":"completed","jobs_completed":8,"jobs_failed":0,"completed_at":"2026-07-23T09:30:00.000Z"}}}}},"responses":{"200":{"description":"Return any 2xx within 10 seconds to acknowledge."}}}},"article.published":{"post":{"operationId":"webhookArticlePublished","summary":"An article on one of your projects was published","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"whd_01h9x8","event":"article.published","created_at":"2026-07-23T12:00:05.000Z","data":{"article_id":"art_01h9x5","project_id":"proj_01h9x2","title":"How to Migrate a WordPress Site Without Losing Rankings","slug":"wordpress-migration-seo","language":"en","url":"https://example.com/blog/wordpress-migration-seo","published_at":"2026-07-23T12:00:00.000Z"}}}}},"responses":{"200":{"description":"Return any 2xx within 10 seconds to acknowledge."}}}},"article.generated":{"post":{"operationId":"webhookArticleGenerated","summary":"Reserved: an article finished generating","description":"In the event catalog and accepted as a subscription filter; deliveries for this event begin when the corresponding emitter ships. Same envelope and signature scheme as all events.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"}}}},"responses":{"200":{"description":"Return any 2xx within 10 seconds to acknowledge."}}}},"subscription.updated":{"post":{"operationId":"webhookSubscriptionUpdated","summary":"A project subscription on your account was activated/renewed","description":"Sent when a Creem payment activates or renews one of your projects (checkout completed, subscription active/paid). Same envelope and signature scheme as all events.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"whd_01h9x9","event":"subscription.updated","created_at":"2026-07-23T10:00:05.000Z","data":{"project_id":"proj_01h9x2","status":"active","paid_until":"2026-08-23T10:00:00.000Z","first_activation":true,"creem_event":"checkout.completed"}}}}},"responses":{"200":{"description":"Return any 2xx within 10 seconds to acknowledge."}}}},"credits.updated":{"post":{"operationId":"webhookCreditsUpdated","summary":"A credit pack purchase landed on your balance","description":"Sent when a paid credit-pack checkout completes and the credits are added to your account balance. Same envelope and signature scheme as all events.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"whd_01h9xa","event":"credits.updated","created_at":"2026-07-23T10:05:05.000Z","data":{"pack_id":"pack_2000","credits_added":2200,"balance_credits":2660}}}}},"responses":{"200":{"description":"Return any 2xx within 10 seconds to acknowledge."}}}},"usage.threshold_reached":{"post":{"operationId":"webhookUsageThresholdReached","summary":"Reserved: a usage threshold on your account was reached","description":"In the event catalog and accepted as a subscription filter; deliveries begin with the billing phase. Same envelope and signature scheme as all events.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"}}}},"responses":{"200":{"description":"Return any 2xx within 10 seconds to acknowledge."}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"API key in the Authorization header: `Authorization: Bearer sk_sandbox_...` (trial) or `Bearer sk_live_...` (paid). Keys are obtained from POST /v1/accounts (instant sandbox key) or POST /v1/api-keys. The raw key is shown exactly once at creation — only a hash is stored. Sandbox keys expire after 14 days."}},"schemas":{"Error":{"type":"object","description":"Uniform error body returned by every non-2xx response. `code` is machine-readable (full catalog: https://serp-agent.com/docs/errors); `doc_url` links its documentation.","required":["error"],"properties":{"error":{"type":"object","required":["code","message","doc_url"],"properties":{"code":{"type":"string","example":"rate_limited"},"message":{"type":"string","description":"Human/agent-readable explanation, often with the exact next step."},"doc_url":{"type":"string","format":"uri"}}}}},"SandboxLimits":{"type":"object","description":"Hard caps on a sandbox trial key.","required":["audits","articles","expires_days"],"properties":{"audits":{"type":"integer","example":1},"articles":{"type":"integer","example":2},"expires_days":{"type":"integer","example":14}}},"SignupResponse":{"type":"object","required":["account_id","api_key","mode","sandbox_limits","upgrade_url","docs"],"properties":{"account_id":{"type":"string"},"api_key":{"type":"string","description":"Raw sandbox key (sk_sandbox_…). Shown exactly once — store it now."},"mode":{"type":"string","const":"sandbox"},"sandbox_limits":{"$ref":"#/components/schemas/SandboxLimits"},"upgrade_url":{"type":"string","format":"uri"},"docs":{"type":"string","format":"uri"}}},"ApiKey":{"type":"object","required":["id","name","key_prefix","last4","mode","created_at","last_used_at","expires_at","revoked_at"],"properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"key_prefix":{"type":"string","enum":["sk_sandbox_","sk_live_"]},"last4":{"type":"string","description":"Last 4 characters of the raw key."},"mode":{"type":"string","enum":["sandbox","live"]},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time","description":"Sandbox keys expire; live keys have null."},"revoked_at":{"type":["string","null"],"format":"date-time"}}},"CreatedApiKey":{"type":"object","description":"ApiKey plus the raw secret — present only in the creation response.","required":["id","name","key_prefix","last4","mode","created_at","last_used_at","expires_at","revoked_at","api_key"],"properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"key_prefix":{"type":"string","enum":["sk_sandbox_","sk_live_"]},"last4":{"type":"string"},"mode":{"type":"string","enum":["sandbox","live"]},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"},"api_key":{"type":"string","description":"Full raw key — shown exactly once."}}},"ApiKeyList":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}},"ApiKeyEnvelope":{"type":"object","required":["key"],"properties":{"key":{"$ref":"#/components/schemas/ApiKey"}}},"Account":{"type":"object","required":["account_id","email","email_verified","mode","plan","sandbox_usage","sandbox_limits","created_at","projects_count"],"properties":{"account_id":{"type":"string"},"email":{"type":"string","format":"email"},"email_verified":{"type":"boolean"},"mode":{"type":"string","enum":["sandbox","live"],"description":"Mode of the key used on this request."},"plan":{"type":"null","description":"Always null for now — populated by the upcoming billing phase."},"sandbox_usage":{"type":["object","null"],"description":"Trial units consumed. Null for live keys.","properties":{"audits":{"type":"integer"},"articles":{"type":"integer"}}},"sandbox_limits":{"oneOf":[{"$ref":"#/components/schemas/SandboxLimits"},{"type":"null"}],"description":"Null for live keys."},"created_at":{"type":"string","format":"date-time"},"projects_count":{"type":"integer"}}},"Project":{"type":"object","required":["project_id","url","name","status","language","country","sandbox","access","created_at"],"properties":{"project_id":{"type":"string"},"url":{"type":"string","format":"uri"},"name":{"type":"string"},"status":{"type":"string","enum":["setup","running","completed","paused"],"description":"Lifecycle of the project's background processing."},"language":{"type":"string","example":"en"},"country":{"type":"string","example":"US"},"sandbox":{"type":"boolean","description":"True for projects created with a sandbox key (draft-only articles, trial budgets)."},"access":{"type":"string","enum":["active","pending_payment","sandbox"],"description":"Billing gate: audits/articles require \"active\" or \"sandbox\". \"pending_payment\" projects return 402 on work endpoints."},"created_at":{"type":"string","format":"date-time"},"counts":{"type":"object","description":"Present on list/detail responses.","properties":{"keywords":{"type":"integer"},"articles":{"type":"integer"},"audits":{"type":"integer"}}}}},"ProjectList":{"type":"object","required":["projects"],"properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}},"LatestAudit":{"type":["object","null"],"description":"Latest COMPLETED technical audit, or null when none has completed.","properties":{"audit_id":{"type":"string"},"status":{"type":"string","enum":["queued","running","completed","failed"]},"completed_at":{"type":["string","null"],"format":"date-time"},"score":{"type":["integer","null"],"description":"Overall score 0–100."},"summary":{"oneOf":[{"$ref":"#/components/schemas/AuditSummary"},{"type":"null"}]}}},"ProjectDetail":{"type":"object","description":"Project (with counts) plus latest_audit.","required":["project_id","url","name","status","language","country","sandbox","access","created_at","counts","latest_audit"],"properties":{"project_id":{"type":"string"},"url":{"type":"string","format":"uri"},"name":{"type":"string"},"status":{"type":"string","enum":["setup","running","completed","paused"]},"language":{"type":"string"},"country":{"type":"string"},"sandbox":{"type":"boolean"},"access":{"type":"string","enum":["active","pending_payment","sandbox"]},"created_at":{"type":"string","format":"date-time"},"counts":{"type":"object","properties":{"keywords":{"type":"integer"},"articles":{"type":"integer"},"audits":{"type":"integer"}}},"latest_audit":{"$ref":"#/components/schemas/LatestAudit"}}},"AuditAccepted":{"type":"object","required":["audit_id","status","poll"],"properties":{"audit_id":{"type":"string"},"status":{"type":"string","enum":["queued","running"],"description":"\"running\" when an already-active run was returned."},"poll":{"type":"string","description":"Path to poll: GET /v1/audits/{audit_id}."}}},"AuditSummary":{"type":"object","description":"Summarized audit report — severity counts, top issues (grouped per check, capped at 10, worst first), and per-category scores.","required":["issues_critical","issues_warning","issues_info","top_issues","categories"],"properties":{"issues_critical":{"type":"integer"},"issues_warning":{"type":"integer"},"issues_info":{"type":"integer"},"top_issues":{"type":"array","items":{"type":"object","required":["check","severity","title","affected_count"],"properties":{"check":{"type":"string","description":"Check type, e.g. SSL, PAGESPEED, SITEMAP, CANONICAL, MOBILE."},"severity":{"type":"string","enum":["critical","warning","info"]},"title":{"type":"string"},"affected_count":{"type":"integer"}}}},"categories":{"type":"object","description":"Average score 0–100 per category (performance, security, indexability, mobile, structure, other). Only categories with scored checks appear.","additionalProperties":{"type":["integer","null"]}}}},"Audit":{"type":"object","required":["audit_id","project_id","status","started_at","completed_at"],"properties":{"audit_id":{"type":"string"},"project_id":{"type":"string"},"status":{"type":"string","enum":["queued","running","completed","failed"]},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"score":{"type":["integer","null"],"description":"Overall score 0–100. Present only when completed."},"summary":{"oneOf":[{"$ref":"#/components/schemas/AuditSummary"},{"type":"null"}],"description":"Present only when completed."},"error":{"type":"string","description":"Present only when failed."}}},"ArticleAccepted":{"type":"object","required":["article_id","status","poll"],"properties":{"article_id":{"type":"string"},"status":{"type":"string","const":"queued"},"poll":{"type":"string","description":"Path to poll: GET /v1/articles/{article_id}."}}},"Article":{"type":"object","required":["article_id","project_id","status"],"properties":{"article_id":{"type":"string"},"project_id":{"type":"string"},"status":{"type":"string","enum":["generating","qa","ready","published","failed"],"description":"generating → qa → ready. \"published\" if/when the operator publishes it. Content fields appear at ready/published."},"title":{"type":["string","null"]},"meta_description":{"type":["string","null"]},"content_markdown":{"type":["string","null"],"description":"Full article body as markdown."},"word_count":{"type":["integer","null"]},"qa_score":{"type":["integer","null"],"description":"Internal QA score 0–100."},"language":{"type":"string"},"error":{"type":"string","description":"Present only when failed."},"watermark":{"type":"object","description":"Present on sandbox projects — the article is a draft-only trial output.","properties":{"sandbox":{"type":"boolean","const":true},"note":{"type":"string"}}}}},"PlansDocument":{"type":"object","description":"Machine-readable pricing catalog: subscription plans (per project, per month), the agent purchase channel, an a-la-carte unit catalog, sandbox trial terms, and links. See GET /v1/plans for the live document.","required":["currency","updated_at","plans","agent_channel","a_la_carte","sandbox","links"],"properties":{"currency":{"type":"string","const":"USD"},"updated_at":{"type":"string","format":"date-time","description":"Catalog version stamp — bumped when prices/limits change."},"merchant_of_record":{"type":"string"},"plans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["basic","pro"]},"name":{"type":"string"},"audience":{"type":"string"},"monthly_price":{"type":"number"},"currency":{"type":"string"},"period":{"type":"string"},"featured":{"type":"boolean"},"features":{"type":"array","items":{"type":"string"}},"checkout_url":{"type":"string","format":"uri"}}}},"agent_channel":{"type":"object","description":"Per-project programmatic purchase of the same plans via POST /v1/subscriptions; status at GET /v1/subscription."},"a_la_carte":{"type":"object","description":"Individual SEO work units, paid with prepaid credits (packs at POST /v1/credits/purchases, balance at GET /v1/credits)."},"sandbox":{"type":"object","description":"Free trial terms."},"links":{"type":"object"}}},"SubscriptionCheckout":{"type":"object","description":"A pending subscription purchase. Hand checkout_url to a human; the project activates via webhook on payment.","required":["status","checkout_url","project_id","plan"],"properties":{"status":{"type":"string","const":"pending_payment"},"checkout_url":{"type":"string","format":"uri","description":"Hosted Creem checkout — a human completes payment here."},"project_id":{"type":"string"},"plan":{"type":"string","enum":["basic","pro"]},"note":{"type":"string"}}},"SubscriptionStatus":{"type":"object","required":["project_id","plan","status"],"properties":{"project_id":{"type":"string"},"plan":{"type":["string","null"],"enum":["basic","pro",null],"description":"Null when no subscription or the tier cannot be resolved."},"status":{"type":"string","enum":["active","pending_payment","none"],"description":"active = live paid month (work endpoints all-inclusive); pending_payment = API checkout in flight; none = no subscription (pay per unit with credits, or subscribe)."},"paid_until":{"type":"string","format":"date-time","description":"Present when active."},"sandbox":{"type":"boolean","description":"Present (true) on sandbox projects — they run on the free trial budget, no subscription."},"note":{"type":"string"}}},"CreditsOverview":{"type":"object","required":["balance_credits","currency","credit_usd_value","unit_prices","packs","purchase"],"properties":{"balance_credits":{"type":"integer","description":"Whole credits available (1 credit = $1 = 100 cents of User balance)."},"currency":{"type":"string","const":"USD"},"credit_usd_value":{"type":"number","const":1},"unit_prices":{"type":"object","description":"Price in credits per work unit.","additionalProperties":{"type":"integer"}},"packs":{"type":"array","items":{"type":"object","required":["id","usd","credits","bonus_credits","available"],"properties":{"id":{"type":"string","enum":["pack_500","pack_2000","pack_10000"]},"usd":{"type":"number"},"credits":{"type":"integer"},"bonus_credits":{"type":"integer","description":"Free credits on top of the dollar price (volume discount)."},"available":{"type":"boolean","description":"False while the operator has not configured the pack's Creem product (purchase would 503)."}}}},"purchase":{"type":"string","const":"POST /v1/credits/purchases"},"notes":{"type":"array","items":{"type":"string"}}}},"CreditPackCheckout":{"type":"object","description":"A pending credit-pack purchase. Hand checkout_url to a human; credits land via webhook on payment.","required":["status","checkout_url","pack_id","usd","credits","bonus_credits"],"properties":{"status":{"type":"string","const":"pending_payment"},"checkout_url":{"type":"string","format":"uri"},"pack_id":{"type":"string","enum":["pack_500","pack_2000","pack_10000"]},"usd":{"type":"number"},"credits":{"type":"integer"},"bonus_credits":{"type":"integer"},"note":{"type":"string"}}},"WebhookCreated":{"type":"object","required":["webhook_id","url","events","active","created_at","secret","signature_header","signature_scheme"],"properties":{"webhook_id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"},"description":"[] = subscribed to all events."},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"secret":{"type":"string","description":"Raw signing secret (64 hex chars) — shown exactly once, never retrievable again."},"signature_header":{"type":"string","const":"X-SerpAgent-Signature"},"signature_scheme":{"type":"string","const":"hex sha256 HMAC of the raw request body"}}},"WebhookEndpoint":{"type":"object","required":["webhook_id","url","events","active","secret","failure_count","last_success_at","last_failure_at","created_at"],"properties":{"webhook_id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean","description":"False after 50 consecutive delivery failures (auto-deactivated) — delete and re-register to resume."},"secret":{"type":"string","description":"Always masked in listings."},"failure_count":{"type":"integer","description":"Consecutive delivery failures; reset to 0 on success."},"last_success_at":{"type":["string","null"],"format":"date-time"},"last_failure_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"WebhookList":{"type":"object","required":["webhooks"],"properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"WebhookDeleted":{"type":"object","required":["deleted","webhook_id"],"properties":{"deleted":{"type":"boolean","const":true},"webhook_id":{"type":"string"}}},"WebhookEnvelope":{"type":"object","description":"Delivery envelope POSTed to your endpoint for every event. Headers: X-SerpAgent-Signature (hex sha256 HMAC of the raw body, keyed with your endpoint secret), X-SerpAgent-Event (event name), X-SerpAgent-Delivery (delivery id — stable across retries, dedupe on it). Respond 2xx within 10s or the delivery is retried with exponential backoff (max 8 attempts).","required":["id","event","created_at","data"],"properties":{"id":{"type":"string","description":"Delivery id (same value as X-SerpAgent-Delivery)."},"event":{"type":"string","enum":["audit.completed","article.generated","article.published","subscription.updated","usage.threshold_reached","credits.updated"]},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object","description":"Event-specific payload — see the webhooks section examples."}}}},"headers":{"X-RateLimit-Limit":{"description":"Request budget for the current window (600/min per API key on authenticated routes; 5/hour per IP on signup).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"\"true\" when this response was replayed from a stored Idempotency-Key result instead of re-executing.","schema":{"type":"string","enum":["true"]}}}},"x-sandbox-limits":{"description":"Hard caps on a free sandbox trial (per key): 1 project, 1 audit, 2 draft-only articles; the key expires after 14 days. Exceeding a cap returns 403 sandbox_limit_reached with the upgrade URL. Upgrade: https://serp-agent.com/pricing","projects":1,"audits":1,"articles":2,"expires_days":14}}