> ## Documentation Index
> Fetch the complete documentation index at: https://devs.surferseo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Use cases

> Create and share Content Editors in bulk, publish a draft to a CMS, generate an article with outline review, and optimize an existing page.

You need an API key and the id of an active workspace; the [Quickstart](/api/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](/api/async-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](/api/v2/content-editors/create-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](/api/requests#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](/api/v2/content-editors/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](/api/v2/content-editors/get-content-editor) until `state` is `completed` or `failed`.
3. Call [List Content Editor permalinks](/api/v2/permalinks/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](/integrations/embed/access-and-sharing#connect-to-an-existing-content-editor)).

Create one editor; run it once per row with that row's key:

```bash theme={"system"}
curl -X POST "https://app.surferseo.com/api/v2/workspaces/<workspace_id>/content_editors" \
  -H "API-KEY: $SURFER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: q4-plan-row-001" \
  -d '{"main_keyword": "best crm for small businesses"}'
```

The `201` response carries the editor's `id` and current `state`; step 2 waits for it to finish.

### Revoke share links

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](/api/v2/permalinks/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](/api/v2/content-editors/get-content-editor) first; [Recognizing a fresh score](/api/async-and-webhooks#recognizing-a-fresh-score) tells you when the value is current.

1. Call [Get Content Editor content](/api/v2/content-editors/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:

```bash theme={"system"}
curl "https://app.surferseo.com/api/v2/workspaces/<workspace_id>/content_editors/<content_editor_id>/content" \
  -H "API-KEY: $SURFER_API_KEY" \
  -H "Accept: text/markdown" \
  -o draft.md
```

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](/api/rate-limits#credits)).

1. Create the editor with [Create Content Editor](/api/v2/content-editors/create-content-editor).
2. Wait for `content_editor.initialization.completed`, or poll [Get Content Editor](/api/v2/content-editors/get-content-editor) until `state` is `completed` or `failed`; stop on `failed`.
3. Call [Create AI Article](/api/v2/ai-articles/create-ai-article) with `{"manual_outline": true}`.
4. Wait for `content_editor.ai_article.waiting_for_user_input`, or poll [Get AI Article](/api/v2/ai-articles/get-ai-article) until `state` is `waiting_for_user_input` or `failed`; stop on `failed`.
5. Read the outline with [Get AI Article outline](/api/v2/ai-articles/get-ai-article-outline) and `Accept: text/markdown`.
6. Edit the outline.
7. Submit it with [Submit edited AI Article outline](/api/v2/ai-articles/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](/api/v2/ai-articles/get-ai-article) until `state` is `completed` or `failed`; stop on `failed`.
9. Read the article with [Get Content Editor content](/api/v2/content-editors/get-content-editor-content).

Start the article and pause it at the outline:

```bash theme={"system"}
curl -X POST "https://app.surferseo.com/api/v2/workspaces/<workspace_id>/content_editors/<content_editor_id>/ai_articles" \
  -H "API-KEY: $SURFER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"manual_outline": true}'
```

The `201` response carries the article's `id` and `state` `new`; step 4 waits for the outline. [AI Article states](/api/async-and-webhooks#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.

> [!WARNING]
>
> 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](/api/v2/content-editors/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](/api/v2/content-editors/get-content-editor) until `state` is `completed` or `failed`; stop on `failed`.
3. Read the starting score with [Get SEO Guidelines score](/api/v2/seo-guidelines/get-seo-guidelines-score) and keep its `calculated_at`.

Create the editor from a live URL:

```bash theme={"system"}
curl -X POST "https://app.surferseo.com/api/v2/workspaces/<workspace_id>/content_editors" \
  -H "API-KEY: $SURFER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"main_keyword": "how to build a gaming pc", "import_content_url": "https://example.com/gaming-pc-guide"}'
```

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](/api/v2/auto-optimize/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](/api/v2/auto-optimize/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](/api/v2/content-editors/get-content-editor-content).
5. Read the score again with [Get SEO Guidelines score](/api/v2/seo-guidelines/get-seo-guidelines-score) once `status` is `ready` and `calculated_at` is newer than the value you kept.

[Recognizing a fresh score](/api/async-and-webhooks#recognizing-a-fresh-score) explains the `status` and `calculated_at` contract, and [Auto-Optimize states](/api/async-and-webhooks#auto-optimize-states) describes each job state.

## Recipes that stay on v1

These recipes use v1 with the same key; [Migrating from v1](/api/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](/api/rate-limits#credits)).

### Audit score for a URL

Audit scores a live page against a keyword and its top competitors.

1. Call [Create Audit query](/api/v1/audit/create-audit-query) with `url` and `keyword`. The `201` response carries `id`, `state` `scheduled`, and a `permalink_hash`.
2. Poll [Get Audit query](/api/v1/audit/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](/api/rate-limits#credits)).

1. Call [Create SERP Analyzer query](/api/v1/serp-analyzer/create-serp-analyzer-query-post-create) with `keyword`. The `201` response carries `id` and `state` `scheduled`.
2. Poll [List SERP Analyzer queries for a workspace](/api/v1/serp-analyzer/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](/api/v1/serp-analyzer/get-search-results) and [Get prominent terms](/api/v1/serp-analyzer/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](/api/v1/other/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](/api/v1/other/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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.