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

  • 400Malformed JSON or invalid field. Code: `invalid_request`.
  • 409Email already registered. Code: `account_exists`.
  • 429Too many signups from this IP (5/hour). Code: `rate_limited`.
  • 500Unexpected 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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429Rate 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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429Rate 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

  • 400Malformed body (`invalid_request`) or 5 active keys already exist (`key_limit_reached`).
  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429Rate 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

  • 400This is the only active key. Code: `cannot_revoke_last_key`.
  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404No such key on this account. Code: `not_found`.
  • 429Rate 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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429Rate 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

  • 400Malformed body or invalid URL. Code: `invalid_request`.
  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 403Sandbox keys are limited to 1 project. Code: `sandbox_limit_reached`.
  • 429Rate 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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404No such project on this account. Code: `not_found`.
  • 429Rate 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

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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404No such audit on this account. Code: `not_found`.
  • 429Rate 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

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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404No such article on this account. Code: `not_found`.
  • 429Rate 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

  • 400Malformed body or unknown plan_id. Code: `invalid_request`.
  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 403Sandbox keys cannot make purchases. Code: `sandbox_not_billable`.
  • 404No such project on this account. Code: `not_found`.
  • 409The project already has an active subscription. Code: `already_subscribed`.
  • 429Rate limit exceeded (600 requests/minute per API key). Honor `Retry-After`.
  • 503Billing 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

  • 400Missing project_id. Code: `invalid_request`.
  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404No such project on this account. Code: `not_found`.
  • 429Rate 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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429Rate 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

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

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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 429Rate 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

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

  • 401Missing, malformed, unknown, revoked, or expired API key. Codes: `unauthorized`, `key_revoked`, `key_expired`.
  • 404No such endpoint on this account. Code: `not_found`.
  • 429Rate 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.