API reference
Endpoints for projects, search terms and runs in the Serpfull REST API v1.
Base URL: https://www.serpfull.com/api/v1. Send your key in the Authorization header. Responses are JSON; lists look like { "object": "list", "data": [...] }. A project is one of your websites.
Projects
GET /projects
GET /projects/{project_id}Search terms
GET /projects/{project_id}/terms
POST /projects/{project_id}/terms (read & write key)
POST /terms (read & write key; project_id in the body)
GET /terms/{term_id}
PATCH /terms/{term_id} (read & write key)
DELETE /terms/{term_id} (read & write key)Create a search term
curl -X POST https://www.serpfull.com/api/v1/projects/$PROJECT/terms \
-H "Authorization: Bearer $SERPFULL_KEY" -H "Content-Type: application/json" \
-d '{"keyword":"emergency plumber leeds","country":"GB","device":"mobile","clicks_per_day":10}'Create a search term with POST /terms
curl -X POST https://www.serpfull.com/api/v1/terms \
-H "Authorization: Bearer $SERPFULL_KEY" -H "Content-Type: application/json" \
-d '{"project_id":"$PROJECT","keyword":"boiler repair leeds","clicks_per_day":20,
"schedule":{"timezone":"Europe/London","active_hours":{"start":9,"end":18},
"start_date":"2026-11-01","ramp_up":{"start_clicks":2,"increase_per_day":2}}}'POST /terms needs a read & write key (403 insufficient_scope otherwise) and returns 201 with the search term. It takes the same fields as POST /projects/{project_id}/terms plus project_id (required). Plan limits are checked first: going over the project's search-term or clicks-per-day limit returns 422 limit_exceeded with what's left; a project without a plan returns 422 plan_required; an unverified website returns 422 project_unverified.
Scheduling (optional schedule object, on create and PATCH): timezone (IANA name, default Europe/London); active_hours { start 0–23, end 1–24 } or null for the website's hours; start_date and end_date (YYYY-MM-DD, inclusive, in that timezone); ramp_up { start_clicks, increase_per_day } or null, where clicks start at start_clicks on day one and rise by increase_per_day until clicks_per_day. status may be "active" (default) or "paused" on create. On PATCH, schedule replaces the whole schedule.
Fields: keyword (required), target_url (a page on the project's domain, or null), country (ISO 3166-1 alpha-2; one of the countries offered in the app's Country list; default GB), device (desktop, mobile or tablet; default desktop). target_url may be on the domain or one of its subdomains, clicks_per_day (0–1,000, default 5). PATCH accepts the same fields plus status: "active" or "paused".
SEO audit
GET /projects/{project_id}/seo pages, limits and usage
GET /seo/pages/{page_id} page, latest audit (issues and facts), competitor pages
GET /seo/pages/{page_id}/audits?limit=50 audit history, newest first
POST /seo/pages/{page_id}/audits start an audit (read & write key)
GET /seo/audits/{audit_id} one audit with category scores, issues and factsPOST /seo/pages/{page_id}/audits returns 202 with { "object": "seo_audit", "id": "…", "status": "running" }; poll GET /seo/audits/{id} until status is completed or failed (usually under a minute). It applies the same limits as the app. When one applies you get 429 with error.code cooldown, concurrency, daily_quota, monthly_quota, competitor_quota or host_busy, error.retry_at and a Retry-After header; 403 when the website needs an active plan or verification.
Runs
GET /runs?project_id=&term_id=&outcome=&since=&until=&limit=50&cursor=Runs are newest first and default to the last 30 days. limit is 1–200 (default 50). since and until are ISO 8601 timestamps. When has_more is true, pass next_cursor as cursor to get the next page. outcome is one of the values in Run outcomes explained.
Example run
{
"id": "…", "object": "run", "project_id": "…", "term_id": "…",
"status": "finished", "outcome": "completed", "position": 4, "page": 1,
"dwell_seconds": 48, "pages_visited": 2, "device": "mobile", "country": "GB", "error_code": null,
"started_at": "2026-10-20T13:12:00Z", "finished_at": "2026-10-20T13:13:05Z"
}Updated