Skip to main content
Send v2 requests to 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 Editor 456 in workspace 123:
v1 stays available at 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):
A 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.
In a terminal, replace the content of Content Editor 456 in workspace 123 with the Markdown file article.md:
The 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 whose state 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 active or was deleted.
  • The id is not a number.
The workspace check never answers 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:
The response is a list, the shape the next section describes.

Lists

A list response is an object with data, 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:
When 156 editors match, the response carries this meta object:

Which lists are paginated

Six lists are paginated; the rest return every item, and their meta 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 with filter[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 form filter[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:
The -g flag stops curl from reading the brackets as a URL pattern. In the response, meta.filters is {"state": ["completed", "failed"]}.

Ordering

Paginated lists take sort 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 an Idempotency-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:
Run it twice, and the second run is the first row of the table: 201 with the same voice.