REST API reference

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

This page is rendered from the machine-readable spec at /openapi.json (OpenAPI 3.1.0). Base URL: https://serp-agent.com. All /v1 responses carry X-RateLimit-Limit / -Remaining / -Reset headers; 429s carry Retry-After. Error codes: /docs/errors.

Accounts

Programmatic signup and account status.

post/v1/accounts

Create an account (programmatic signup)

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`.

Auth: none (public endpoint)

Sandbox limits: audits: 1 · articles: 2 · projects: 1 · expires_days: 14

ParameterInRequiredDescription
Idempotency-KeyheadernoAny 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.

Request body (application/json)

FieldTypeRequiredDescription
emailstringyesTrimmed and lowercased server-side.
company_namestringno
originstringnoWho is signing up. Defaults to "agent". One of: "agent", "human".

Example request

curl -X POST https://serp-agent.com/v1/accounts \
  -H "Content-Type: application/json" \
  -d '{"email":"agent@example.com","company_name":"Acme Agents","origin":"agent"}'

Example response — 201 (Account created. `api_key` is the raw sandbox key — it is shown exactly once and never retrievable again.)

{
  "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"
}

Errors

  • 400 — Malformed JSON or invalid field. Code: `invalid_request`.
  • 409 — Email already registered. Code: `account_exists`.
  • 429 — Too many signups from this IP (5/hour). Code: `rate_limited`.
  • 500 — Unexpected server error — safe to retry with the same Idempotency-Key. Code: `internal_error`.

get/v1/account

Get the calling account

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).

Auth: Authorization: Bearer sk_…

Example request

curl https://serp-agent.com/v1/account \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (The account snapshot.)

{
  "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
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

API keys

Create, list, and revoke API keys (max 5 active per account).

get/v1/api-keys

List API keys

All keys on the account, newest first. Only `key_prefix` + `last4` are exposed — the full secret and hash never appear after creation.

Auth: Authorization: Bearer sk_…

Example request

curl https://serp-agent.com/v1/api-keys \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (The account's keys.)

{
  "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
    }
  ]
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

post/v1/api-keys

Create an additional API key

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).

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
Idempotency-KeyheadernoAny 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.

Request body (application/json)

FieldTypeRequiredDescription
namestringno

Example request

curl -X POST https://serp-agent.com/v1/api-keys \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-runner"}'

Example response — 201 (Key created. `api_key` is the full raw key — shown exactly once.)

{
  "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"
}

Errors

  • 400 — Malformed body (`invalid_request`) or 5 active keys already exist (`key_limit_reached`).
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

delete/v1/api-keys/{id}

Revoke an API key

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).

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
idpathyesAPI key id (from POST/GET /v1/api-keys).

Example request

curl -X DELETE https://serp-agent.com/v1/api-keys/{id} \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (Key revoked (or already revoked).)

{
  "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"
  }
}

Errors

  • 400 — This is the only active key. Code: `cannot_revoke_last_key`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such key on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

Projects

Sites/domains the API works on. Sandbox keys: 1 project.

get/v1/projects

List projects

All projects on the account, newest first, each with keyword/article/audit counts.

Auth: Authorization: Bearer sk_…

Example request

curl https://serp-agent.com/v1/projects \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (The account's projects.)

{
  "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
      }
    }
  ]
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

post/v1/projects

Create a project

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".

Auth: Authorization: Bearer sk_…

Sandbox limits: projects: 1

ParameterInRequiredDescription
Idempotency-KeyheadernoAny 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.

Request body (application/json)

FieldTypeRequiredDescription
urlstringyesThe site to work on. Normalized server-side; must be a valid http(s) URL.
namestringno
languagestringnoPrimary content language (lowercased). Default "en".
countrystringnoPrimary target country (uppercased). Default "US".

Example request

curl -X POST https://serp-agent.com/v1/projects \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","name":"Example","language":"en","country":"US"}'

Example response — 201 (Project created.)

{
  "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"
}

Errors

  • 400 — Malformed body or invalid URL. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 403 — Sandbox keys are limited to 1 project. Code: `sandbox_limit_reached`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

get/v1/projects/{projectId}

Get a project

Project details including counts and the latest completed technical-audit summary (`latest_audit`, null when no audit has completed yet).

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
projectIdpathyesProject id.

Example request

curl https://serp-agent.com/v1/projects/{projectId} \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (Project details.)

{
  "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
      }
    }
  }
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such project on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

Audits

Technical SEO audit runs (async: enqueue, then poll).

post/v1/projects/{projectId}/audits

Start a technical audit

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.

Auth: Authorization: Bearer sk_…

Sandbox limits: audits: 1

ParameterInRequiredDescription
projectIdpathyesProject id.
Idempotency-KeyheadernoAny 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.

Example request

curl -X POST https://serp-agent.com/v1/projects/{projectId}/audits \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 202 (Audit queued (or an already-active run returned). Poll the `poll` path.)

{
  "audit_id": "aud_01h9x4",
  "status": "queued",
  "poll": "/v1/audits/aud_01h9x4"
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 402 — 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`).
  • 403 — Sandbox audit budget exhausted (1 per trial). Code: `sandbox_limit_reached`.
  • 404 — No such project on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

get/v1/audits/{auditId}

Poll an audit run

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`.

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
auditIdpathyesAudit id from POST /v1/projects/{projectId}/audits.

Example request

curl https://serp-agent.com/v1/audits/{auditId} \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (Current audit state.)

{
  "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
    }
  }
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such audit on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

Articles

Single-article SEO content generation (async: enqueue, then poll).

post/v1/projects/{projectId}/articles

Generate one SEO article

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.

Auth: Authorization: Bearer sk_…

Sandbox limits: articles: 2

ParameterInRequiredDescription
projectIdpathyesProject id.
Idempotency-KeyheadernoAny 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.

Request body (application/json)

FieldTypeRequiredDescription
topicstringnoWorking title / topic. Placed verbatim as the article's planned title.
primary_keywordstringnoTarget keyword. Derived from topic (lowercased) when omitted.

Example request

curl -X POST https://serp-agent.com/v1/projects/{projectId}/articles \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"topic":"How to migrate a WordPress site without losing rankings","primary_keyword":"wordpress migration seo"}'

Example response — 202 (Article generation queued. Poll the `poll` path.)

{
  "article_id": "art_01h9x5",
  "status": "queued",
  "poll": "/v1/articles/art_01h9x5"
}

Errors

  • 400 — Malformed body, or neither topic nor primary_keyword given. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 402 — 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`).
  • 403 — Sandbox article budget exhausted (2 per trial). Code: `sandbox_limit_reached`.
  • 404 — No such project on this account. Code: `not_found`.
  • 409 — An article is already generating for this project. Code: `article_in_progress`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

get/v1/articles/{articleId}

Poll an article generation

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.

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
articleIdpathyesArticle id from POST /v1/projects/{projectId}/articles.

Example request

curl https://serp-agent.com/v1/articles/{articleId} \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (Current article state.)

{
  "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"
  }
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such article on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

Plans

Public machine-readable pricing.

get/v1/plans

Get pricing (public)

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.

Auth: none (public endpoint)

Example request

curl https://serp-agent.com/v1/plans

Example response — 200 (The pricing document. (Response example abridged — fetch the endpoint for the full catalog.))

{
  "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"
  }
}

Billing

Per-project subscriptions and prepaid credits (Creem checkout — a human completes payment in the browser).

post/v1/subscriptions

Buy a per-project subscription

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`.

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
Idempotency-KeyheadernoAny 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.

Request body (application/json)

FieldTypeRequiredDescription
project_idstringyes
plan_idstringyesbasic = $199/mo, pro = $299/mo (see GET /v1/plans). One of: "basic", "pro".

Example request

curl -X POST https://serp-agent.com/v1/subscriptions \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id":"proj_01h9x2","plan_id":"pro"}'

Example response — 201 (Checkout created — hand `checkout_url` to a human to complete payment.)

{
  "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."
}

Errors

  • 400 — Malformed body or unknown plan_id. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 403 — Sandbox keys cannot make purchases. Code: `sandbox_not_billable`.
  • 404 — No such project on this account. Code: `not_found`.
  • 409 — The project already has an active subscription. Code: `already_subscribed`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.
  • 503 — Billing for this plan is not configured on the server yet. Code: `billing_not_configured`.

get/v1/subscription

Get a project's subscription state

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).

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
project_idqueryyesProject id.

Example request

curl https://serp-agent.com/v1/subscription \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (The project's subscription state.)

{
  "project_id": "proj_01h9x2",
  "plan": "pro",
  "status": "active",
  "paid_until": "2026-08-23T09:00:00.000Z"
}

Errors

  • 400 — Missing project_id. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such project on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

get/v1/credits

Get credit balance, unit prices, and packs

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).

Auth: Authorization: Bearer sk_…

Example request

curl https://serp-agent.com/v1/credits \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (Balance + catalog.)

{
  "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."
  ]
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

post/v1/credits/purchases

Buy a prepaid credit pack

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`.

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
Idempotency-KeyheadernoAny 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.

Request body (application/json)

FieldTypeRequiredDescription
pack_idstringyes One of: "pack_500", "pack_2000", "pack_10000".

Example request

curl -X POST https://serp-agent.com/v1/credits/purchases \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pack_id":"pack_2000"}'

Example response — 201 (Checkout created — hand `checkout_url` to a human to complete payment.)

{
  "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."
}

Errors

  • 400 — Missing or unknown pack_id. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 403 — Sandbox keys cannot make purchases. Code: `sandbox_not_billable`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.
  • 503 — This pack's Creem product is not configured yet. Code: `billing_not_configured`.

Conversions

Report real leads from your own server, so articles can be judged by business outcome and not only by clicks.

post/v1/projects/{projectId}/conversions

Report a conversion from your server

Reports one real conversion — a lead your own systems already accepted. The browser tracker (one <script> line, see https://serp-agent.com/docs/tracking) can only observe that a form was SENT; a form backend, CRM webhook or booking system knows whether the lead was accepted, which is what makes a server report trustworthy enough to count as a business outcome. Attribution: the conversion is credited to one article, in this order — `articleUrl` (explicit), `ctaTrackingId`, `pageUrl`, then `landingUrl` (the page the visit started on, which is what usually names the article). The response says which one was used. A conversion that matches no article is still stored and counted for the project. Idempotency: pass `externalId` — your own lead id. A repeat with the same id is ignored and answered 200 with `duplicate: true`, so retries and webhook redeliveries cannot inflate the numbers. Never send personal data: no names, emails, phone numbers or message bodies. Only the event type, the pages involved and your own lead id.

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
projectIdpathyesProject id.

Request body (application/json)

FieldTypeRequiredDescription
eventTypestringyesWhat happened. FORM_SUBMIT and CALENDAR_BOOKED are the ones a server normally reports. One of: "FORM_SUBMIT", "CALENDAR_BOOKED", "PHONE_CLICK", "EMAIL_CLICK", "CTA_CLICK".
externalIdstringnoYour own id for this lead. Makes the call idempotent — a repeat is ignored.
pageUrlstringnoPage the conversion happened on.
landingUrlstringnoPage the visit started on. Usually the article that earned the lead.
articleUrlstringnoExplicit article URL when you already know which article to credit. Strongest evidence.
occurredAtstringnoWhen it happened (ISO 8601). Defaults to now; must not be more than 5 minutes in the future.
ctaTrackingIdstringnoValue of the data-sa-cta attribute on the button, when your pages use one.
utm_sourcestringno
utm_mediumstringno
utm_campaignstringno
utm_contentstringno

Example request

curl -X POST https://serp-agent.com/v1/projects/{projectId}/conversions \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"eventType":"FORM_SUBMIT","externalId":"lead_88421","pageUrl":"https://example.com/contact","landingUrl":"https://example.com/blog/how-to-choose-movers","occurredAt":"2026-09-18T09:12:04Z"}'

Example response — 200 (Already reported under this `externalId` — nothing was stored a second time.)

{
  "recorded": false,
  "duplicate": true,
  "article_attributed": true,
  "attribution": "none"
}

Errors

  • 400 — Malformed body, unknown eventType, a URL that is not a URL, or occurredAt in the future. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such project on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

Webhooks

Outbound event notifications with HMAC signatures.

get/v1/webhooks

List webhook endpoints

All endpoints on the account, oldest first. The signing secret is masked (shown only at creation).

Auth: Authorization: Bearer sk_…

Example request

curl https://serp-agent.com/v1/webhooks \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (The account's endpoints.)

{
  "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"
    }
  ]
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

post/v1/webhooks

Register a webhook endpoint

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.

Auth: Authorization: Bearer sk_…

Request body (application/json)

FieldTypeRequiredDescription
urlstringyesPublic HTTPS URL to POST events to.
eventsarraynoEvents to deliver. Omit or [] = all events.

Example request

curl -X POST https://serp-agent.com/v1/webhooks \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://agent.example.com/hooks/serp-agent","events":["audit.completed","article.published"]}'

Example response — 201 (Endpoint registered. `secret` is shown exactly once — store it to verify signatures.)

{
  "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"
}

Errors

  • 400 — Invalid URL (must be public https), unknown event name, or malformed body. Code: `invalid_request`.
  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 409 — Endpoint cap reached (20 per account). Code: `limit_reached`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.
  • 500 — Server-side secret encryption unavailable. Code: `internal_error`.

delete/v1/webhooks/{id}

Delete a webhook endpoint

Removes the endpoint and its queued deliveries.

Auth: Authorization: Bearer sk_…

ParameterInRequiredDescription
idpathyesWebhook endpoint id.

Example request

curl -X DELETE https://serp-agent.com/v1/webhooks/{id} \
  -H "Authorization: Bearer $SERP_AGENT_API_KEY"

Example response — 200 (Endpoint deleted.)

{
  "deleted": true,
  "webhook_id": "wh_01h9x6"
}

Errors

  • 401 — Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404 — No such endpoint on this account. Code: `not_found`.
  • 429 — Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.

Webhook events

Register endpoints with POST /v1/webhooks. Every delivery is an HTTPS POST carrying X-SerpAgent-Signature (hex sha256 HMAC of the raw request body, keyed with your endpoint secret), X-SerpAgent-Event, and X-SerpAgent-Delivery (stable across retries — deduplicate on it). Respond with any 2xx within 10 seconds; failures are retried with exponential backoff (max 8 attempts, capped at 6 hours between attempts). After 50 consecutive failures the endpoint is deactivated.

audit.completed

An audit run for one of your projects finished

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.

{
  "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"
  }
}

article.published

An article on one of your projects was published

{
  "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"
  }
}

article.generated

Reserved: an article finished generating

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.

subscription.updated

A project subscription on your account was activated/renewed

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.

{
  "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"
  }
}

credits.updated

A credit pack purchase landed on your balance

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.

{
  "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
  }
}

usage.threshold_reached

Reserved: a usage threshold on your account was reached

In the event catalog and accepted as a subscription filter; deliveries begin with the billing phase. Same envelope and signature scheme as all events.