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
| Parameter | In | Required | Description |
|---|---|---|---|
| Idempotency-Key | header | no | 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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| string | yes | Trimmed and lowercased server-side. | |
| company_name | string | no | |
| origin | string | no | Who 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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| Idempotency-Key | header | no | 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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | no |
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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| id | path | yes | API 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
| Parameter | In | Required | Description |
|---|---|---|---|
| Idempotency-Key | header | no | 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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | yes | The site to work on. Normalized server-side; must be a valid http(s) URL. |
| name | string | no | |
| language | string | no | Primary content language (lowercased). Default "en". |
| country | string | no | Primary 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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| projectId | path | yes | Project 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
| Parameter | In | Required | Description |
|---|---|---|---|
| projectId | path | yes | Project id. |
| Idempotency-Key | header | no | 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. |
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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| auditId | path | yes | Audit 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
| Parameter | In | Required | Description |
|---|---|---|---|
| projectId | path | yes | Project id. |
| Idempotency-Key | header | no | 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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| topic | string | no | Working title / topic. Placed verbatim as the article's planned title. |
| primary_keyword | string | no | Target 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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| articleId | path | yes | Article 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/plansExample 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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| Idempotency-Key | header | no | 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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| project_id | string | yes | |
| plan_id | string | yes | basic = $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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| project_id | query | yes | Project 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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| Idempotency-Key | header | no | 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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| pack_id | string | yes | 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`.
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)
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | yes | Public HTTPS URL to POST events to. |
| events | array | no | Events 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_…
| Parameter | In | Required | Description |
|---|---|---|---|
| id | path | yes | Webhook 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.