## Content Editors

### GET /api/v2/content_editors
List all Content Editors
Returns a paginated list of Content Editors across all workspaces for the organization.

Subject to a 90-day time-range filter limit.
Unknown filter keys are ignored. Applied filters are reflected in the response `meta.filters` object.

**Rate limit**: 10 requests/sec

**Parameters**

- `page` (query, integer) — Page number (1-indexed).
- `page_size` (query, integer) — Number of items per page.
- `sort` (query, string) — Field to sort by.
- `order` (query, string) — Sort direction.
- `filter[state]` (query, string) — Filter by state.
- `filter[inserted_after]` (query, string) — Only return Content Editors created after this ISO8601 timestamp.
- `filter[inserted_before]` (query, string) — Only return Content Editors created before this ISO8601 timestamp.

**Responses**

- **200** — Paginated list of Content Editors
```json
{
  "data": [
    {
      "error": null,
      "id": 123,
      "state": "completed",
      "device": "mobile",
      "location": "United States",
      "inserted_at": "2024-04-12T15:23:45Z",
      "updated_at": "2024-06-01T09:17:33Z",
      "workspace_id": 123,
      "use_brand_knowledge": true,
      "seo_guidelines": null,
      "secondary_keywords": [
        "keyword research",
        "ai visibility"
      ],
      "permalinks": [
        {
          "type": "edit",
          "hash": "abc123xyz",
          "url": "https://app.surferseo.com/drafts/s/abc123xyz"
        }
      ],
      "outline": null,
      "custom_voice_id": null,
      "custom_template_id": null,
      "import_content_url": "https://example.com/article",
      "main_keyword": "seo tools",
      "content_score": {
        "total": 75,
        "ai_search": 62,
        "seo": 75
      },
      "created_by": "user@example.com",
      "target_word_count": 2000,
      "auto_optimize": null,
      "ai_article": null,
      "surfer_template": null,
      "ai_search_guidelines": null
    }
  ],
  "meta": {
    "total": 156,
    "sort": "inserted_at",
    "filters": {
      "inserted_after": "2024-04-12T15:23:45Z",
      "state": "completed"
    },
    "order": "desc",
    "page_size": 25,
    "page": 1,
    "total_pages": 7
  }
}
```
- **400** — Validation error
```json
{
  "error": {
    "message": "Request validation failed",
    "reason": "validation_error",
    "details": [
      {
        "reason": "invalid",
        "field": "field_name"
      }
    ]
  }
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```


### GET /api/v2/workspaces/{workspace_id}/content_editors
List Content Editors in workspace
Returns a paginated list of Content Editors for the workspace.

Subject to a 90-day time-range filter limit.
Unknown filter keys are ignored. Applied filters are reflected in the response `meta.filters` object.

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `page` (query, integer) — Page number (1-indexed).
- `page_size` (query, integer) — Number of items per page.
- `sort` (query, string) — Field to sort by.
- `order` (query, string) — Sort direction.
- `filter[state]` (query, string) — Filter by state.
- `filter[inserted_after]` (query, string) — Only return Content Editors created after this ISO8601 timestamp.
- `filter[inserted_before]` (query, string) — Only return Content Editors created before this ISO8601 timestamp.

**Responses**

- **200** — Paginated list of Content Editors
```json
{
  "data": [
    {
      "error": null,
      "id": 123,
      "state": "completed",
      "device": "mobile",
      "location": "United States",
      "inserted_at": "2024-04-12T15:23:45Z",
      "updated_at": "2024-06-01T09:17:33Z",
      "workspace_id": 123,
      "use_brand_knowledge": true,
      "seo_guidelines": null,
      "secondary_keywords": [
        "keyword research",
        "ai visibility"
      ],
      "permalinks": [
        {
          "type": "edit",
          "hash": "abc123xyz",
          "url": "https://app.surferseo.com/drafts/s/abc123xyz"
        }
      ],
      "outline": null,
      "custom_voice_id": null,
      "custom_template_id": null,
      "import_content_url": "https://example.com/article",
      "main_keyword": "seo tools",
      "content_score": {
        "total": 75,
        "ai_search": 62,
        "seo": 75
      },
      "created_by": "user@example.com",
      "target_word_count": 2000,
      "auto_optimize": null,
      "ai_article": null,
      "surfer_template": null,
      "ai_search_guidelines": null
    }
  ],
  "meta": {
    "total": 156,
    "sort": "inserted_at",
    "filters": {
      "inserted_after": "2024-04-12T15:23:45Z",
      "state": "completed"
    },
    "order": "desc",
    "page_size": 25,
    "page": 1,
    "total_pages": 7
  }
}
```
- **400** — Validation error
```json
{
  "error": {
    "message": "Request validation failed",
    "reason": "validation_error",
    "details": [
      {
        "reason": "invalid",
        "field": "field_name"
      }
    ]
  }
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **404** — Workspace not found
```json
{
  "error": {
    "message": "Resource not found",
    "reason": "not_found",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```


### POST /api/v2/workspaces/{workspace_id}/content_editors
Create Content Editor
Creates a new Content Editor. The operation is asynchronous — the response returns immediately
with the Content Editor in `scheduled` state.

`use_brand_knowledge` defaults to `true`. When enabled, workspace brand knowledge is used where available; set `false` for unbranded output.

Supports `Idempotency-Key` header.

**Keywords:** `main_keyword` (required) is the primary keyword that drives the SERP analysis.
Add up to 19 `secondary_keywords` (20 keywords total including the main keyword); exceeding the
limit returns `400` with a `secondary_keywords` / `too_many` detail.

**Locations:** `location` is a country or region display name (e.g. `"United States"`, the default
when omitted). Fetch the full list of supported values from `GET /api/v1/locations` (no auth
required). `device` selects the SERP device profile — `mobile` (default) or `desktop`.

**Templates:** `surfer_template` and `custom_template_id` are mutually exclusive. When neither
is provided, Surfer asynchronously preselects a template — the workspace's default custom
template if configured, otherwise a Surfer template inferred from the SERP. Read the final
values after `state` becomes `completed`. On a completed Content Editor, both fields `null`
represents the "SERP-based" template: the article structure follows the SERP analysis rather
than a preset.

**Custom voice:** when `custom_voice_id` is omitted, the workspace's default custom voice (if any)
is used. Pass `null` explicitly to opt out.

**Webhooks:** `content_editor.initialization.completed` / `content_editor.initialization.failed`
fire when initialization settles. During initialization the AI Search score is computed, firing
`content_editor.ai_search_score.calculated` / `.failed`; when content is imported at creation the
SEO score is computed too, firing `content_editor.seo_score.calculated` / `.failed` followed by
`content_editor.content_score.recalculated` (the unified score). As a fallback, poll
`GET .../content_editors/:id`.

**Cost:** each successful request consumes one Content Editor credit.

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `Idempotency-Key` (header, string) — Client-generated key for safely retrying create requests. Reusing the same key for the same path returns the original response; reusing it for a different path returns 422. Keys expire after 24 hours.

**Request body**

```json
{
  "device": "mobile",
  "location": "United States",
  "use_brand_knowledge": true,
  "secondary_keywords": [
    "keyword research",
    "best seo software"
  ],
  "notes": "Draft for Q2 campaign",
  "custom_voice_id": null,
  "custom_template_id": null,
  "custom_instructions": "Mention product X, write from perspective of Y",
  "import_content_url": "https://example.com/article",
  "main_keyword": "seo tools",
  "target_word_count": 2000,
  "surfer_template": null
}
```

**Responses**

- **201** — Created Content Editor
```json
{
  "error": null,
  "id": 123,
  "state": "scheduled",
  "device": "mobile",
  "location": "United States",
  "inserted_at": "2024-04-12T15:23:45Z",
  "updated_at": "2024-06-01T09:17:33Z",
  "workspace_id": 123,
  "use_brand_knowledge": true,
  "seo_guidelines": null,
  "secondary_keywords": [
    "keyword research",
    "ai visibility"
  ],
  "permalinks": [
    {
      "type": "edit",
      "hash": "abc123xyz",
      "url": "https://app.surferseo.com/drafts/s/abc123xyz"
    }
  ],
  "outline": null,
  "notes": "Draft for Q2 campaign",
  "custom_voice_id": null,
  "custom_template_id": null,
  "custom_instructions": "Write from perspective of a senior developer",
  "import_content_url": "https://example.com/article",
  "main_keyword": "seo tools",
  "content_score": {
    "total": 75,
    "ai_search": 62,
    "seo": 75
  },
  "created_by": "user@example.com",
  "meta_description": "A curated list of the top SEO tools for 2025.",
  "target_word_count": 2000,
  "auto_optimize": null,
  "ai_article": null,
  "meta_title": "Best SEO Tools in 2025",
  "surfer_template": null,
  "ai_search_guidelines": null
}
```
- **400** — Validation error
```json
{
  "error": {
    "message": "Request validation failed",
    "reason": "validation_error",
    "details": [
      {
        "reason": "invalid",
        "field": "field_name"
      }
    ]
  }
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **404** — Workspace not found
```json
{
  "error": {
    "message": "Resource not found",
    "reason": "not_found",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **415** — Unsupported media type
```json
{
  "error": {
    "message": "Request content type is not supported",
    "reason": "unsupported_media_type",
    "details": []
  }
}
```
- **422** — Quota exceeded
```json
{
  "error": {
    "message": "Quota exceeded",
    "reason": "quota_exceeded",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```


### GET /api/v2/workspaces/{workspace_id}/content_editors/{content_editor_id}/content
Get Content Editor content
Returns the raw content of the Content Editor. Requires the editor to be in `completed` state.

Response format negotiated via the `Accept` header:

| Accept Header         | Response Format |
|-----------------------|-----------------|
| `text/html` (default) | HTML            |
| `text/markdown`       | Markdown        |

The response is always encoded as UTF-8, regardless of any `charset` parameter in the `Accept` header.

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `content_editor_id` (path, integer, required)

**Responses**

- **200** — Raw content (HTML or Markdown)
- **400** — Validation error
```json
{
  "error": {
    "message": "Request validation failed",
    "reason": "validation_error",
    "details": [
      {
        "reason": "invalid",
        "field": "field_name"
      }
    ]
  }
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **404** — Not found
```json
{
  "error": {
    "message": "Resource not found",
    "reason": "not_found",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **409** — Content Editor not completed
```json
{
  "error": {
    "message": "Content Editor not completed",
    "reason": "conflict",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```
- **500** — Internal error
```json
{
  "error": {
    "message": "An unexpected error occurred",
    "reason": "internal_error",
    "details": []
  }
}
```


### PUT /api/v2/workspaces/{workspace_id}/content_editors/{content_editor_id}/content
Update Content Editor content
Replaces the content of the Content Editor. Requires the editor to be in `completed` state.

Request body format negotiated via the `Content-Type` header:

| Content-Type Header   | Body Format |
|-----------------------|-------------|
| `text/html` (default) | HTML        |
| `text/markdown`       | Markdown    |

The request body is always treated as UTF-8, regardless of any `charset` parameter in the `Content-Type` header.

Triggers an asynchronous recalculation of the Content Score, including both the SEO and AI Search scores. Each fires its own
webhook when its score settles (`content_editor.seo_score.calculated`/`.failed` and
`content_editor.ai_search_score.calculated`/`.failed`); once both have settled the
`content_editor.content_score.recalculated` webhook fires with the unified Content Score. As a
fallback, poll each score's `status` — the SEO score on `GET .../seo_guidelines/score`, the AI
Search score on `GET .../ai_search_guidelines/score` — or read the unified `content_score` on
`GET .../content_editors/:id`.

**Content sanitization**: the body is parsed and re-serialized server-side; the stored content may differ from what was submitted. Markdown bodies go through the same pipeline (parsed, then re-serialized).

Common transformations:

| Transformation | Example |
| -- | -- |
| HTML tag re-balancing | `<p><p></p>` becomes `<p></p><p></p>` |
| Disallowed tags stripped | `<iframe>`, `<script>`, and similar are dropped |
| Disallowed attributes stripped | attributes on otherwise-allowed tags may be removed |

The response is `204 No Content` regardless of any rewrites — **callers cannot infer what was modified from the response**. Re-fetch `GET /content_editors/:id/content` to inspect the canonical stored form.

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `content_editor_id` (path, integer, required)

**Responses**

- **204** — Content updated. Body is sanitized and re-serialized; response does not echo any rewrites — re-fetch to inspect.
- **400** — Validation error
```json
{
  "error": {
    "message": "Request validation failed",
    "reason": "validation_error",
    "details": [
      {
        "reason": "invalid",
        "field": "field_name"
      }
    ]
  }
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **404** — Not found
```json
{
  "error": {
    "message": "Resource not found",
    "reason": "not_found",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **409** — Content Editor not completed
```json
{
  "error": {
    "message": "Content Editor not completed",
    "reason": "conflict",
    "details": []
  }
}
```
- **413** — Content too large
```json
{
  "error": {
    "message": "Content too large",
    "reason": "content_too_large",
    "details": []
  }
}
```
- **415** — Unsupported media type
```json
{
  "error": {
    "message": "Request content type is not supported",
    "reason": "unsupported_media_type",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```
- **500** — Internal error
```json
{
  "error": {
    "message": "An unexpected error occurred",
    "reason": "internal_error",
    "details": []
  }
}
```


### DELETE /api/v2/workspaces/{workspace_id}/content_editors/{id}
Delete Content Editor
Deletes a Content Editor. Idempotent — deleting an already-deleted Content Editor returns 204.

**Cost:** deletion does not refund the Content Editor credit, and the deleted Content Editor still counts toward [fair usage](https://docs.surferseo.com/en/articles/12944161-fair-usage-policy).

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `id` (path, integer, required)

**Responses**

- **204** — Deleted
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```


### GET /api/v2/workspaces/{workspace_id}/content_editors/{id}
Get Content Editor
Returns a single Content Editor by ID.

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `id` (path, integer, required)

**Responses**

- **200** — Content Editor detail
```json
{
  "error": null,
  "id": 123,
  "state": "completed",
  "device": "mobile",
  "location": "United States",
  "inserted_at": "2024-04-12T15:23:45Z",
  "updated_at": "2024-06-01T09:17:33Z",
  "workspace_id": 123,
  "use_brand_knowledge": true,
  "seo_guidelines": null,
  "secondary_keywords": [
    "keyword research",
    "ai visibility"
  ],
  "permalinks": [
    {
      "type": "edit",
      "hash": "abc123xyz",
      "url": "https://app.surferseo.com/drafts/s/abc123xyz"
    }
  ],
  "outline": null,
  "notes": "Draft for Q2 campaign",
  "custom_voice_id": null,
  "custom_template_id": null,
  "custom_instructions": "Write from perspective of a senior developer",
  "import_content_url": "https://example.com/article",
  "main_keyword": "seo tools",
  "content_score": {
    "total": 75,
    "ai_search": 62,
    "seo": 75
  },
  "created_by": "user@example.com",
  "meta_description": "A curated list of the top SEO tools for 2025.",
  "target_word_count": 2000,
  "auto_optimize": null,
  "ai_article": null,
  "meta_title": "Best SEO Tools in 2025",
  "surfer_template": null,
  "ai_search_guidelines": null
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **404** — Not found
```json
{
  "error": {
    "message": "Resource not found",
    "reason": "not_found",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```


### PATCH /api/v2/workspaces/{workspace_id}/content_editors/{id}
Customize Content Editor
Updates Content Editor settings. Only provided fields are changed. Requires the Content Editor
to be in `completed` state.

**Rate limit**: 10 requests/sec

**Parameters**

- `workspace_id` (path, integer, required)
- `id` (path, integer, required)

**Request body**

```json
{
  "use_brand_knowledge": false,
  "notes": "Updated notes",
  "custom_voice_id": null,
  "custom_template_id": null,
  "custom_instructions": "Write from perspective of Y",
  "surfer_template": null
}
```

**Responses**

- **200** — Updated Content Editor
```json
{
  "error": null,
  "id": 123,
  "state": "completed",
  "device": "mobile",
  "location": "United States",
  "inserted_at": "2024-04-12T15:23:45Z",
  "updated_at": "2024-06-01T09:17:33Z",
  "workspace_id": 123,
  "use_brand_knowledge": true,
  "seo_guidelines": null,
  "secondary_keywords": [
    "keyword research",
    "ai visibility"
  ],
  "permalinks": [
    {
      "type": "edit",
      "hash": "abc123xyz",
      "url": "https://app.surferseo.com/drafts/s/abc123xyz"
    }
  ],
  "outline": null,
  "notes": "Draft for Q2 campaign",
  "custom_voice_id": null,
  "custom_template_id": null,
  "custom_instructions": "Write from perspective of a senior developer",
  "import_content_url": "https://example.com/article",
  "main_keyword": "seo tools",
  "content_score": {
    "total": 75,
    "ai_search": 62,
    "seo": 75
  },
  "created_by": "user@example.com",
  "meta_description": "A curated list of the top SEO tools for 2025.",
  "target_word_count": 2000,
  "auto_optimize": null,
  "ai_article": null,
  "meta_title": "Best SEO Tools in 2025",
  "surfer_template": null,
  "ai_search_guidelines": null
}
```
- **400** — Validation error
```json
{
  "error": {
    "message": "Request validation failed",
    "reason": "validation_error",
    "details": [
      {
        "reason": "invalid",
        "field": "field_name"
      }
    ]
  }
}
```
- **401** — Unauthorized
```json
{
  "error": {
    "message": "Missing or invalid API key",
    "reason": "unauthorized",
    "details": []
  }
}
```
- **403** — Forbidden
```json
{
  "error": {
    "message": "Permission denied",
    "reason": "permission_denied",
    "details": []
  }
}
```
- **404** — Not found
```json
{
  "error": {
    "message": "Resource not found",
    "reason": "not_found",
    "details": []
  }
}
```
- **406** — Not acceptable
```json
{
  "error": {
    "message": "Requested response content type is not supported",
    "reason": "not_acceptable",
    "details": []
  }
}
```
- **409** — Content Editor not completed
```json
{
  "error": {
    "message": "Content Editor not completed",
    "reason": "conflict",
    "details": []
  }
}
```
- **415** — Unsupported media type
```json
{
  "error": {
    "message": "Request content type is not supported",
    "reason": "unsupported_media_type",
    "details": []
  }
}
```
- **429** — Rate limit exceeded
```json
{
  "error": {
    "message": "Rate limit exceeded",
    "reason": "rate_limit_exceeded",
    "details": [
      {
        "retry_after": 60
      }
    ]
  }
}
```


## Legacy v1

### GET /api/v1/content_editors
Legacy v1 · List Content Editors for a workspace
> **Deprecated.** Use [GET /api/v2/content_editors](/api/v2/docs#tag/Content-Editors/operation/KSeoWeb.Api.V2.ContentEditors.Controller.org_index) for an organization-wide listing or [GET /api/v2/workspaces/{workspace_id}/content_editors](/api/v2/docs#tag/Content-Editors/operation/KSeoWeb.Api.V2.ContentEditors.Controller.index) for a workspace-scoped listing instead.

Retrieves a list of Content Editors for a workspace. It uses workspace ID from the `Workspace-Id` header.
If the `Workspace-Id` header is not provided, uses the oldest active workspace for the user's owned organization.
The request is limited to a 90 days period and paginated.
The `from` and `to` parameters can be used to filter the results by creation date.

**Rate limit**: 10 requests/sec

**Parameters**

- `Workspace-Id` (header, integer) — Workspace ID to list content editors from. If not provided, uses the oldest active workspace.
- `page` (query, integer)
- `page_size` (query, integer) — Number of items per page
- `from` (query, string) — ISO8601 timestamp - returns only drafts created after this time. Defaults to 90 days before `to` date.
- `to` (query, string) — ISO8601 timestamp - returns only drafts created before this time. Defaults to `now` or 90 days after `from` date.

**Responses**

- **200** — 
```json
{
  "data": [
    {
      "id": 123,
      "state": "scheduled",
      "location": "United States",
      "inserted_at": "2021-01-01T12:00:00Z",
      "updated_at": "2021-01-01T12:00:00Z",
      "workspace_id": 1,
      "permalink_hash": "G74SCitlYgE_N8iIYIxIS_US8yuUgsfx",
      "ai_article_status": "new",
      "creator": "john.doe@example.com"
    }
  ],
  "meta": {
    "total": 156,
    "page_size": 25,
    "page": 1,
    "total_pages": 7
  }
}
```
- **422** — 


### POST /api/v1/content_editors
Legacy v1 · Create Content Editor query
> **Deprecated.** Use [POST /api/v2/workspaces/{workspace_id}/content_editors](/api/v2/docs#tag/Content-Editors/operation/KSeoWeb.Api.V2.ContentEditors.Controller.create) instead.

Creates a new Content Editor query with an ability to generate SurferAI article.
Use the `id` from a successful response to poll for state updates, as the process is asynchronous and might take a while to complete. It is only safe to render Surfer Guidelines for queries with `completed` state.

To check for SurferAI query status changes, instead of polling GET requests you can set up a webhook solution. Refer to the [Webhooks section](#section/Webhooks) for details on how to set it up.

You'll receive the following payload in the webhook request body:

- `id` - notification id (integer)
- `type` - one of:
  - `"content_editor.initialization.completed"` - the Content Editor was initialized successfully
  - `"content_editor.initialization.failed"` - the Content Editor initialization failed
  - `"content_editor.content_score.recalculated"` - the Content Score was recalculated (after updating the content via the API)
  - `"content_editor.ai_article.completed"` - whole article finished generating
  - `"content_editor.ai_article.waiting_for_user_input"` - outline finished generating and is waiting for review in app (when `manual_outline: true`)
  - `"content_editor.ai_article.failed"` - generation failed
- `timestamp` - ISO8601 extended UTC timestamp (string)
- `payload`:
  - `draft_id` - unique identifier of the Content Editor (integer)
  - `permalink_hash` - used to share Content Editor with people (string)

We will only notify you of queries that were created via API. If you create a SurferAI query via web app the webhook will not be sent.

**Rate limit**: 10 requests/sec

**Cost:** each successful request will consume one Content Editor (or SurferAI) credit, regardless of the number of keywords specified for the analysis.

**Parameters**

- `Workspace-Id` (header, integer) — Workspace ID to create the content editor in. If not provided, uses the oldest active workspace.

**Request body**

```json
{
  "device": "mobile",
  "location": "United States",
  "keywords": [
    "How to write content that ranks?"
  ],
  "manual_outline": false,
  "word_count": 2137,
  "write_with_ai": false,
  "import_content_from_url": "https://surferseo.com/"
}
```

**Responses**

- **201** — 
```json
{
  "id": 123,
  "state": "scheduled",
  "workspace_id": 1,
  "permalink_hash": "G74SCitlYgE_N8iIYIxIS_US8yuUgsfx",
  "ai_article_status": "new"
}
```
- **422** — Quota exceeded | No access | AI article quota exceeded
- **429** — Rate limit exceeded


### GET /api/v1/content_editors/{id}
Legacy v1 · Get Content Editor query
> **Deprecated.** Use [GET /api/v2/workspaces/{workspace_id}/content_editors/{id}](/api/v2/docs#tag/Content-Editors/operation/KSeoWeb.Api.V2.ContentEditors.Controller.show) instead.

Use this endpoint to poll for state updates.

**Rate limit**: 10 requests/sec

**Parameters**

- `id` (path, integer, required)

**Responses**

- **200** — 
```json
{
  "id": 123,
  "state": "scheduled",
  "workspace_id": 1,
  "permalink_hash": "G74SCitlYgE_N8iIYIxIS_US8yuUgsfx",
  "ai_article_status": "new"
}
```
- **404** — Not found
- **429** — Rate limit exceeded


### PATCH /api/v1/content_editors/{id}
Legacy v1 · Update Content Editor query
> **Deprecated.** This endpoint has been split in v2. Use [PUT /api/v2/workspaces/{workspace_id}/content_editors/{id}/content](/api/v2/docs#tag/Content-Editors/operation/KSeoWeb.Api.V2.ContentEditors.Content.Controller.update) to update content, or [PATCH /api/v2/workspaces/{workspace_id}/content_editors/{id}/seo_guidelines/terms](/api/v2/docs#tag/SEO-Guidelines/operation/KSeoWeb.Api.V2.ContentEditors.SeoGuidelines.Terms.Controller.update) to update included terms.

Updates the content or included terms for the Content Editor.
The editor must be in a `completed` state. Use the `Get Content Editor query` endpoint to check its status.

**Rate limit**: 10 requests/sec

A recalculated Content Score will be sent via webhook. Refer to the [Webhooks section](#section/Webhooks) for details on how to set it up.

You'll receive the following payload in the webhook request body:

- `id` - notification id (integer)
- `type` - `"content_editor.content_score.recalculated"`
- `timestamp` - ISO8601 extended UTC timestamp (string)
- `payload`:
  - `draft_id` - unique identifier of the Content Editor (integer)
  - `permalink_hash` - used to share Content Editor with people (string)
  - `content_score` - updated content score (integer)


**Parameters**

- `id` (path, integer, required)

**Request body**

```json
{
  "content": "<h1>Title</h1><p>First paragraph</p>",
  "included_terms": [
    "SEO",
    "on-page optimization",
    "SERP",
    "Google algorithm"
  ]
}
```

**Responses**

- **200** — OK
- **404** — Not found
- **409** — Content Editor is not completed
- **429** — Rate limit exceeded


### POST /api/v1/content_editors/{id}/auto_optimize
Legacy v1 · Auto-Optimize Content Editor query
> **Deprecated.** Use [POST /api/v2/workspaces/{workspace_id}/content_editors/{id}/auto_optimize](/api/v2/docs#tag/Auto-Optimize/operation/KSeoWeb.Api.V2.ContentEditors.AutoOptimize.Controller.start) instead.

Runs Auto-Optimize on the Content Editor query. All changes will be written directly to the Content Editor.

To check for Auto-Optimize run status changes, use webhooks. Refer to the [Webhooks section](#section/Webhooks) for details on how to set it up.

You'll receive the following payload in the webhook request body:

- `id` - notification id (integer)
- `type` - one of:
  - `"content_editor.auto_optimize.completed"`
  - `"content_editor.auto_optimize.failed"`
  - `"content_editor.auto_optimize.cancelled"`
- `payload`:
  - `job_id` - reference number of the Auto-Optimize run (integer)
  - `draft_id` - unique identifier of the Content Editor (integer)
  - `result` (only when type is `"content_editor.auto_optimize.completed"`) - `"optimized"` if article has been optimized, `"nothing_to_optimize"` otherwise

- `timestamp` - ISO8601 extended UTC timestamp (string)

**Rate limit**: 10 requests/sec

**Parameters**

- `id` (path, integer, required)

**Responses**

- **200** — OK
```json
{
  "job_id": 0
}
```
- **204** — No sections to optimize
- **402** — No Auto-Optimize credits
- **404** — Content Editor not found
- **409** — Content Editor is not completed
- **429** — Rate limit exceeded


### GET /api/v1/content_editors/{id}/content
Legacy v1 · Get content inside the Content Editor
> **Deprecated.** Use [GET /api/v2/workspaces/{workspace_id}/content_editors/{id}/content](/api/v2/docs#tag/Content-Editors/operation/KSeoWeb.Api.V2.ContentEditors.Content.Controller.show) instead.

The editor must be in a `completed` state. Use the `Get Content Editor query` endpoint to check its status.

**Rate limit**: 10 requests/sec

**Parameters**

- `id` (path, integer, required)

**Responses**

- **200** — 
```json
{
  "content": "<h1>Title</h1><p>First paragraph</p>"
}
```
- **404** — Not found
- **429** — Rate limit exceeded


### GET /api/v1/content_editors/{id}/content_score
Legacy v1 · Get content score for the Content Editor
> **Deprecated.** Use [GET /api/v2/workspaces/{workspace_id}/content_editors/{id}/seo_guidelines/score](/api/v2/docs#tag/SEO-Guidelines/operation/KSeoWeb.Api.V2.ContentEditors.SeoGuidelines.Controller.score) instead.

The editor must be in a `completed` state. Use the `Get Content Editor Query` endpoint to check its status.

**Rate limit**: 10 requests/sec

**Parameters**

- `id` (path, integer, required)

**Responses**

- **200** — 
```json
{
  "content_score": 75
}
```
- **404** — Not found


### GET /api/v1/content_editors/{id}/structural_guidelines
Legacy v1 · Get structural guidelines for the Content Editor
> **Deprecated.** Use [GET /api/v2/workspaces/{workspace_id}/content_editors/{id}/seo_guidelines/structure](/api/v2/docs#tag/SEO-Guidelines/operation/KSeoWeb.Api.V2.ContentEditors.SeoGuidelines.Controller.structure) instead.

Retrieves the recommended word, character, heading, paragraph and image count for the Content Editor.

The editor must be in a `completed` state. Use the `Get Content Editor query` endpoint to check its status.

**Rate limit**: 10 requests/sec

**Parameters**

- `id` (path, integer, required)

**Responses**

- **200** — 
```json
{
  "word_count": {
    "max": null,
    "min": 0
  },
  "paragraph_count": {
    "max": null,
    "min": 0
  },
  "img_count": {
    "max": null,
    "min": 0
  },
  "char_count": {
    "max": null,
    "min": 0
  },
  "heading_count": {
    "max": null,
    "min": 0
  }
}
```
- **404** — Not found
- **429** — Rate limit exceeded


### GET /api/v1/content_editors/{id}/terms
Legacy v1 · Get all terms for the Content Editor
> **Deprecated.** Use [GET /api/v2/workspaces/{workspace_id}/content_editors/{id}/seo_guidelines/terms](/api/v2/docs#tag/SEO-Guidelines/operation/KSeoWeb.Api.V2.ContentEditors.SeoGuidelines.Terms.Controller.index) instead.

The editor must be in a `completed` state. Use the `Get Content Editor query` endpoint to check its status.

**Rate limit**: 10 requests/sec

**Parameters**

- `id` (path, integer, required)

**Responses**

- **200** — 
```json
{
  "terms": [
    {
      "term": "on-page optimization",
      "ignored": false,
      "included": true,
      "is_nlp": true,
      "target_range": {
        "max": null,
        "min": 0
      },
      "use_in_heading": false
    }
  ]
}
```
- **404** — Not found
- **429** — Rate limit exceeded

