https://app.surferseo.com/api/v2, over HTTPS, with your key in the API-KEY header (Authentication). Each generated reference page adds only what is specific to its endpoint: the request and response fields, and the filters and sort fields its list accepts.
URLs and versions
A v2 URL names the version, the workspace, and the resource. A workspace is the brand or site you manage in Surfer, and a Content Editor is the document Surfer builds around a main keyword and its search analysis (Create Content Editor). This URL addresses Content Editor456 in workspace 123:
https://app.surferseo.com/api/v1 and takes the same key. Migrating from v1 covers what changed between the versions and which tools remain v1 only.
Headers and formats
Resource endpoints take and return JSON; five operations carry an HTML or Markdown document instead, each with its own headers (below).
A
POST that takes no body, such as Start Auto-Optimize, needs no Content-Type.
Timestamps in responses and filters are ISO 8601 in UTC, for example 2024-04-12T15:23:45Z. Every error Surfer handles uses one JSON envelope with a reason and details; Errors documents it and every reason.
In a terminal, create a content template in workspace 123 from the fields saved in template.json (Create content template lists them):
201 returns the new template as JSON.
Document endpoints
Five operations carry the document itself, with no JSON wrapper, and the headers and the success response differ per operation.
A document read with no
Accept returns HTML, and the document is always UTF-8, whatever charset the header carries. A Content-Type that is missing or names another type gets 415.
Accept does not follow Content-Type on the two writes: Update Content Editor content answers 406 to Accept: application/json, and Submit edited AI Article outline returns the AI Article as JSON, so send Accept: application/json or omit Accept.
Two rules constrain a content update before you send it:
- The body is capped at 1,000,000 bytes; a larger one gets
413 content_too_large. - Surfer parses and re-serializes the document before storing it, so the stored form can differ from what you sent.
456 in workspace 123 with the Markdown file article.md:
204 does not echo the stored form; fetch the content again to read it. The update also starts a score recalculation, and Recognizing a fresh score covers how to know when it has settled.
Workspaces in the path
Most resources are workspace-scoped: the workspace id sits in the URL path, and there is no default workspace and no header to set. One organization can hold several workspaces, and one key reaches all of them (what a key can reach). Start with List workspaces to find their ids; only a workspace whosestate is active accepts requests.
Three collections sit above any workspace; everything else lives under one:
A request with a
workspace_id Surfer cannot use gets 404 not_found, with a detail whose reason is entity_not_found and whose entity is workspace. The causes:
- The workspace belongs to another organization.
- The workspace is not
activeor was deleted. - The id is not a number.
403, so a 404 on a path you believe is right points at the id, not the key.
In a terminal, list the custom voices in workspace 123:
Lists
A list response is an object withdata, the items returned, and meta, the counts and the parameters in effect; Which lists are paginated says which lists take the parameters below.
A
page or page_size that is not a positive integer, 0 or abc for example, gets 400 validation_error with the detail field naming the parameter and reason invalid. An empty value falls back to the default.
Fetch the second page of 50 Content Editors in workspace 123:
meta object:
Which lists are paginated
Six lists are paginated; the rest return every item, and theirmeta carries total plus any field the reference page documents.
The 90-day window on Content Editor lists
Both Content Editor lists cover at most 90 days of creation dates; move the window withfilter[inserted_after] and filter[inserted_before].
The window applies to List all Content Editors and List Content Editors in workspace alike, and total counts only the editors inside it.
Both filters take an ISO 8601 timestamp; a value without a zone is read as UTC. The window must start before it ends, end no later than now, and span at most 90 days;
filter[inserted_after] must lie in the past even when sent alone.
A window that breaks a rule gets 400 validation_error with the detail field filter and reason invalid_date_range; a timestamp Surfer cannot parse gets the same status with reason invalid_date_format. To read further back, move the window in 90-day steps and page inside each.
List the Content Editors created in March 2026, across every workspace:
Filtering
A filter is a query parameter of the formfilter[key]=value, and each reference page lists the keys and values its list accepts.
Today List workspaces filters by
type and state, and the two Content Editor lists filter by state, inserted_after, and inserted_before; the reference pages list the values.
In a terminal, list the completed and failed Content Editors in workspace 123, 100 per page:
-g flag stops curl from reading the brackets as a URL pattern. In the response, meta.filters is {"state": ["completed", "failed"]}.
Ordering
Paginated lists takesort and order, and the default is newest first.
An unsupported
sort or order gets 400 validation_error with a detail that names the allowed values; Validation details shows it.
List workspaces in alphabetical order:
Idempotent creates
Three creates take anIdempotency-Key header, and a retry with the same key after a timeout or a dropped connection creates no second resource:
The other POST operations, including starting an AI Article or an Auto-Optimize run, do not take the header; check the resource’s state before starting again (What runs in the background).
The key is any string you generate, a UUID for example. Surfer scopes it to your API key and keeps it for 24 hours after the first request.
Surfer stores the path, the status, and the id of the created resource, not the request body, so a retry with a different body is still a retry. Use a fresh key for every create you intend as new.
Both messages also appear on Errors.
In a terminal, create a custom voice in workspace
123 from the fields saved in voice.json (Create custom voice lists them), with a retry key:
201 with the same voice.