## AI Search

### GET /api/v2/workspaces/{workspace_id}/content_editors/{content_editor_id}/ai_search_guidelines
Get AI Search guidelines for a Content Editor
Returns the AI Search guidelines for the Content Editor — a list of facts gathered from the
SERP and from the major AI engines (Google AI Overviews and AI Mode, Gemini, OpenAI,
Perplexity) for the Content Editor's main keyword and location, with the source URLs and
where each one was cited.

Each source's `cited_by` lists where the URL was found: `serp` for SERP-only sources,
otherwise a subset of `ai_mode`, `ai_overviews`, `gemini`, `openai`, `perplexity`.

`score` is the AI Search score for the current content, 0–100. `null` until the score
has been calculated; recalculated whenever the Content Editor's content changes.

Returns 409 while the Content Editor is `scheduled`, `executing`, or `failed`. Once the
editor is `completed`, returns 200 with the analysis `status`; `facts` populate only when
`status` is `completed`. Poll this endpoint's `status` to detect readiness; the same
value is also surfaced as `ai_search_guidelines.status` on `GET /content_editors/:id`.

**Rate limit**: 10 requests/sec

**Parameters**

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

**Responses**

- **200** — AI Search guidelines
```json
{
  "id": 123,
  "status": "executing",
  "inserted_at": "2024-04-12T15:23:45Z",
  "updated_at": "2024-06-01T09:17:33Z",
  "score": 62,
  "facts": [
    {
      "id": "f1e2c3d4-5678-49ab-9cde-0123456789ab",
      "sources": [
        {
          "url": "https://example.com/article",
          "cited_by": [
            "openai",
            "perplexity"
          ]
        }
      ],
      "fact": "Surfer SEO is an SEO platform with a Content Editor."
    }
  ]
}
```
- **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
      }
    ]
  }
}
```


### GET /api/v2/workspaces/{workspace_id}/content_editors/{content_editor_id}/ai_search_guidelines/facts
List AI Search guidelines facts
Returns the AI Search facts for the Content Editor — items gathered from the SERP and from the major
AI engines (Google AI Overviews and AI Mode, Gemini, OpenAI, Perplexity) for the Content Editor's main
keyword and location, with the source URLs and where each one was cited.

Each source's `cited_by` lists where the URL was found: `serp` for SERP-only sources, otherwise a
subset of `ai_mode`, `ai_overviews`, `gemini`, `openai`, `perplexity`.

`meta.status` reflects the analysis lifecycle: `executing` while SERP analysis or LLM merging is in
progress; `completed` once both have settled and `data` is final — LLM-only sources may be missing
when the LLM merge failed; `failed` if SERP analysis failed. `data` is empty until `status` is
`completed`. The same status is also surfaced as `ai_search_guidelines.status` on
`GET /content_editors/:id`.

**Rate limit**: 10 requests/sec

**Parameters**

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

**Responses**

- **200** — AI Search facts
```json
{
  "data": [
    {
      "fact": "Surfer SEO is an SEO platform with a Content Editor.",
      "id": "f1e2c3d4-5678-49ab-9cde-0123456789ab",
      "sources": [
        {
          "cited_by": [
            "openai",
            "perplexity"
          ],
          "url": "https://example.com/article"
        }
      ]
    }
  ],
  "meta": {
    "status": "completed",
    "total": 1
  }
}
```
- **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
      }
    ]
  }
}
```


### GET /api/v2/workspaces/{workspace_id}/content_editors/{content_editor_id}/ai_search_guidelines/score
Get AI Search score
Returns the AI Search score (0–100), plus its `status` and `calculated_at`. `score` is present for
`ready` and `calculating`, and `null` for `loading`, `error`, and `unavailable`.

`status` values: `ready` once the score is calculated; `loading` while the first calculation is queued
or running (no prior score yet); `calculating` while a recalculation runs over a previously computed
score — the prior `score` is still returned so it can be shown as stale; `error` if calculation failed;
`unavailable` when no score can be produced for this editor.

The score is recalculated whenever the Content Editor's content changes. Prefer the
`content_editor.ai_search_score.calculated` / `.failed` webhook for change events; `status` and
`calculated_at` are there for clients that can't subscribe, or to re-sync after a dropped event.

`calculated_at` is the timestamp of the current `score`. It is `null` until the first calculation
completes — and is also `null` for a `ready` score that was last calculated before this field was
introduced (these are not backfilled; the timestamp populates on the next recalculation).

**Rate limit**: 10 requests/sec

**Parameters**

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

**Responses**

- **200** — AI Search score
```json
{
  "calculated_at": "2026-05-27T10:23:11Z",
  "score": 62,
  "status": "ready"
}
```
- **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
      }
    ]
  }
}
```

