Skip to main content
You need an API key and the id of an active workspace; the Quickstart shows how to get both if you have not made a request yet. The samples read the key from SURFER_API_KEY and use <workspace_id> and <content_editor_id> where your ids go. Every wait step names its webhook event and the GET to poll instead; Async operations and webhooks has the webhook setup.

Create Content Editors from a keyword list and share them with writers

One POST per keyword creates the editors, and one GET per editor returns its share links. A Content Editor is Surfer’s document for one target keyword, with the search analysis, guidelines, and Content Score attached. Each successful create spends one Content Editor credit, so count the rows before you send them. Give each row its own Idempotency-Key and reuse it when you retry that row, so a retry returns the row’s editor instead of a duplicate (idempotent creates). Anyone with an edit link can edit the document, and anyone with a comment link can view and comment, without signing in to Surfer. The links are on app.surferseo.com, or on your white-label domain when your organization has one.
  1. For each keyword, call Create Content Editor with main_keyword and the row’s Idempotency-Key.
  2. Wait for content_editor.initialization.completed for each editor, or poll Get Content Editor until state is completed or failed.
  3. Call List Content Editor permalinks for each completed editor. data holds two links, each with type, hash, and a full url.
  4. Send each writer the edit or comment URL.
To show guidelines inside your CMS instead of sending writers to Surfer, pass the edit link’s hash to the embedded panel (Access and sharing). Create one editor; run it once per row with that row’s key:
The 201 response carries the editor’s id and current state; step 2 waits for it to finish. A reset replaces the links, and the old ones stop working: pass type set to edit or comment to replace one link, or omit it to replace both. Call Reset Content Editor permalinks; the response carries the new links.

Publish a finished draft to a CMS

One GET returns the document as HTML or Markdown, and your code writes it into the CMS. The API has no publish endpoint of its own, and the read spends no credit. Use a completed editor; the call answers 409 until then. If you publish only above a score, check the editor’s content_score on Get Content Editor first; Recognizing a fresh score tells you when the value is current.
  1. Call Get Content Editor content with Accept: text/html or Accept: text/markdown, whichever your CMS takes.
  2. Write the body into your CMS through its own API.
Fetch the document as Markdown and save it to a file:
The 200 body is the document, ready to send to the CMS.

Generate an article with outline review

Start the article with manual_outline set to true, and generation pauses for your outline before it writes. The finished article replaces the editor’s document. Creating the editor counts one Content Editor credit. On Enterprise and some older plans, starting the article spends one AI Article credit and releases the editor’s; other plans include AI Articles in the Content Editor and spend nothing beyond it (Credits).
  1. Create the editor with Create Content Editor.
  2. Wait for content_editor.initialization.completed, or poll Get Content Editor until state is completed or failed; stop on failed.
  3. Call Create AI Article with {"manual_outline": true}.
  4. Wait for content_editor.ai_article.waiting_for_user_input, or poll Get AI Article until state is waiting_for_user_input or failed; stop on failed.
  5. Read the outline with Get AI Article outline and Accept: text/markdown.
  6. Edit the outline.
  7. Submit it with Submit edited AI Article outline and Content-Type: text/markdown. A 200 returns the article, and writing resumes.
  8. Wait for content_editor.ai_article.completed or content_editor.ai_article.failed, or poll Get AI Article until state is completed or failed; stop on failed.
  9. Read the article with Get Content Editor content.
Start the article and pause it at the outline:
The 201 response carries the article’s id and state new; step 4 waits for the outline. AI Article states describes each state the article passes through.

Optimize an existing page

Import the live page into a new editor, run Auto-Optimize, and read back the rewritten document and its new score. The import spends one Content Editor credit, and each successful Auto-Optimize start counts toward your plan’s Auto-Optimize allowance.
Auto-Optimize applies its changes to the document with no review step. Read the result before you publish it.

Import the page

Surfer fetches the URL into the document and, because content is present at creation, computes the SEO score during initialization. A page Surfer cannot fetch leaves the editor failed with error.reason fetch_content_error.
  1. Call Create Content Editor with main_keyword and import_content_url set to the page’s URL.
  2. Wait for content_editor.initialization.completed, or poll Get Content Editor until state is completed or failed; stop on failed.
  3. Read the starting score with Get SEO Guidelines score and keep its calculated_at.
Create the editor from a live URL:
The 201 response carries the editor’s id and current state, as in the first recipe.

Run Auto-Optimize

A new Auto-Optimize run cancels one still running on the same editor. The superseded job reads back as failed, and its webhook event is content_editor.auto_optimize.cancelled.
  1. Call Start Auto-Optimize. The 201 response carries job_id and state executing.
  2. Wait for content_editor.auto_optimize.completed or content_editor.auto_optimize.failed, or poll Get Auto-Optimize job until state is completed or failed; stop on failed.
  3. Read result on the completed job. optimized means Surfer rewrote the document and started a score recalculation; nothing_to_optimize means the document is unchanged, and you are done.
  4. Read the document with Get Content Editor content.
  5. Read the score again with Get SEO Guidelines score once status is ready and calculated_at is newer than the value you kept.
Recognizing a fresh score explains the status and calculated_at contract, and Auto-Optimize states describes each job state.

Recipes that stay on v1

These recipes use v1 with the same key; Migrating from v1 covers how v1 requests differ. Audit and SERP Analyzer answer 403 when your plan lacks the tool and 422 when their allowance is used up. Humanizer answers 422 when its allowance is used up or a run fails, and AI Detector answers 422 when a run fails (Rate limits and credits).

Audit score for a URL

Audit scores a live page against a keyword and its top competitors.
  1. Call Create Audit query with url and keyword. The 201 response carries id, state scheduled, and a permalink_hash.
  2. Poll Get Audit query until state is completed or failed. A completed query returns audited_page.content_score and up to five competitors_pages, each with url and content_score.
To reuse a recent matching query instead of running a new one, pass cache on the create with a lifetime in seconds, 900 or more.

SERP competitor exports

SERP Analyzer returns the top pages for a keyword and the terms they share, as CSV files. Each query counts toward your SERP Analyzer allowance (Rate limits and credits).
  1. Call Create SERP Analyzer query with keyword. The 201 response carries id and state scheduled.
  2. Poll List SERP Analyzer queries for a workspace until the row with your id shows completed or failed; the list is a CSV, and v1 has no per-query read.
  3. Download Get search results and Get prominent terms, both CSV. Each search-results row carries its own state, and failed marks a competitor Surfer could not crawl.

AI detection and humanizing

Both operations return their result in the response.
  • Detect AI content in a text takes text of up to 100,000 characters and returns ai_probability for the whole text and for each chunk.
  • Humanize text takes text of at least 128 characters and returns the rewritten text. Set stream to true to receive it as server-sent events.