---
title: "Pipeline"
description: "The AI generation engine: runs, the content plan, configuration and AI rewrites.  Runs, turning the pipeline on or up, and rewrites spend credits (`x-spends-credits`)."
canonical: "https://writavo.com/docs/api/pipeline"
last-updated: "2026-09-30"
---

# Pipeline

The AI generation engine: runs, the content plan, configuration and AI rewrites.

Runs, turning the pipeline on or up, and rewrites spend credits (`x-spends-credits`).

Base URL: `https://api.writavo.com/v1`

### GET /pipeline/runs

List pipeline runs

- **Operation id**: `listPipelineRuns`
- **Scope**: `pipeline:read`
- **Permission**: `pipeline.run`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Recent engine activity for the Site, newest first. One row per stage invocation, so a
single logical run appears as several rows as work moves through the stages.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `cursor` | query | string | no | The opaque cursor from `data.next_cursor` on the previous page. Do not parse or build one; its encoding will change. |
| `limit` | query | integer | no | Page size. Values above the maximum are clamped, not rejected: ask for a thousand and you get a hundred and a `next_cursor`. |
| `status` | query | PipelineRunStatus | no |  |
| `stage` | query | PipelineStage | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Page | A page of pipeline runs. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/runs \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### POST /pipeline/runs

Request a pipeline run

- **Operation id**: `createPipelineRun`
- **Scope**: `pipeline:run`
- **Permission**: `pipeline.run`
- **Rate limit class**: pipeline
- **Plan feature**: `ai.article_generation`
- **Spends credits**: yes
- **AI agent approval**: `pipeline.run`, always asked
- **Publishable key may call it**: no

**This spends credits.** See "What costs money" in the overview for the other operations
that do.

It asks the engine to work on your Site at the next opportunity, rather than waiting for
the normal cadence. It returns `202` immediately. It does not wait for articles to be
written, and it does not tell you how many will be: what the engine does depends on what
is in your queue and how far the credits go.

Four gates, checked in this order, each with its own error code:

| Gate | Failure | Meaning |
|---|---|---|
| Scope | `403 INSUFFICIENT_SCOPE` | This key does not carry `pipeline:run`. |
| Entitlement | `402 NOT_ENTITLED` | Your plan does not include AI generation. Upgrade. |
| Credits | `402 INSUFFICIENT_CREDITS` | The organisation cannot afford the next unit of work. Top up. |
| Spend cap | `402 SPEND_CAP_REACHED` | This Site hit its own monthly ceiling. Raise it or wait for the reset. |

The order matters: an entitlement failure is answered before a credits failure, so a
caller who is not on the right plan never learns anything about the balance.

Charging happens per unit of work, after that work succeeds, not up front. A run that
exhausts the balance part way through stops at the next stage boundary and finishes as
`partial`. Nothing is rolled back and no article is left half written. Poll
`GET /pipeline/runs/{id}` for the outcome, or subscribe to `pipeline.run.completed`
once webhooks are available.

`Idempotency-Key` is required. Two identical requests within the retention window
produce one run, which is what stops a retried network timeout from spending twice.

**When an AI agent asks.** A key approved for an AI assistant (a device or OAuth sign-in),
or any request from an MCP client, gets `428 APPROVAL_REQUIRED` on the first try, with
`error.approval.url` for a person to approve or deny in the dashboard, whatever the
organisation's approvals switch says: it spends money. Retry the same request with
`Writavo-Approval: <error.approval.id>` once they have. Other callers are unaffected.

While the organisation's plan has no daily spend cap configured, spending is paused and a
run is `409 FEATURE_UNAVAILABLE` ("Spending is paused: this plan has no daily spend cap
configured..."). Report it; retrying will not help.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped to the Site and endpoint and kept for 24 hours. - Same key and body: the original response is replayed, so retrying a timeout is safe. - Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`. - Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. |
| `Writavo-Approval` | header | uuid | no | The `error.approval.id` from a `428 APPROVAL_REQUIRED`, sent on the retry once a person has approved it. Only meaningful for a key approved for an AI agent; every other caller can leave it out. It is single use and bound to the key, to the exact request that asked for it (method, path and body) and to what that request would do: anything else, including a request whose effect changed since it was approved, is `409 APPROVAL_INVALID`. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `max_articles` | integer | no | An upper bound on how many articles this run may produce. Your own safety valve on top of the platform spend cap. Omit to use the Site's configured batch size. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 202 | object | The run was accepted and queued. It runs asynchronously: poll the URL in `Location` (`GET /pipeline/runs/{id}`) until `status` is terminal, or subscribe to the `pipeline.run.*` webhook events instead of polling. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 402 | - | `NOT_ENTITLED`, `INSUFFICIENT_CREDITS` or `SPEND_CAP_REACHED`: allowed, but it cannot be paid for. The fix is, in order: upgrade the plan, top up credits, or raise the Site's cap. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 428 | - | `APPROVAL_REQUIRED` or `APPROVAL_PENDING`. Only ever returned to an AI agent: a key from a device or OAuth sign-in, or any request an MCP client makes (it carries `Writavo-Mcp-Tool`) whatever key it holds. For an operation marked `x-writavo-approval-always` it is returned whatever the organisation's approvals switch says, and for the six content actions while approvals are on. Nothing has happened yet. Show `error.approval.url` to a person; once they approve, repeat the identical request with `Writavo-Approval: <error.approval.id>`. Do not change the body or the path: the approval is bound to the exact request, and a different one is `409 APPROVAL_INVALID`. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/runs \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### GET /pipeline/runs/{id}

Read one pipeline run

- **Operation id**: `getPipelineRun`
- **Scope**: `pipeline:read`
- **Permission**: `pipeline.run`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The outcome of a run. `status: partial` with an `error_summary` is what you see when a
run stopped early, whether because credits ran out, the spend cap was reached, or a
vendor call failed. `items_succeeded` tells you what you did get.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | PipelineRun | The pipeline run. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/runs/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/queue

Read the content queue

- **Operation id**: `listPipelineQueue`
- **Scope**: `pipeline:read`
- **Permission**: `pipeline.run`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

What the engine plans to write, highest priority first. Each item is a topic or keyword
with a source: `manual` if a person added it, `content_gap` if gap analysis found it,
`competitor_seed` if it came from a competitor page.

Add a topic with `POST /pipeline/queue`, change or park one with
`PATCH /pipeline/queue/{id}`, and reorder with `POST /pipeline/queue/reorder`.
`link_opportunity` items come from the link opportunities board.

`signal` carries the structured research behind a `competitor_seed` item: the angle, the
hook, the data points and the gaps. It is extracted signal, never copied prose, which is
the firewall that keeps generated output original.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `cursor` | query | string | no | The opaque cursor from `data.next_cursor` on the previous page. Do not parse or build one; its encoding will change. |
| `limit` | query | integer | no | Page size. Values above the maximum are clamped, not rejected: ask for a thousand and you get a hundred and a `next_cursor`. |
| `status` | query | PlanItemStatus | no |  |
| `source` | query | PlanItemSource | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Page | A page of queue items. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/queue \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### POST /pipeline/queue

Add a topic to the content plan

- **Operation id**: `createPlanItem`
- **Scope**: `plan:write`
- **Permission**: `articles.write`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Add a topic or keyword to the content plan, at `status: planned` and `source: manual`. The plan
is what the pipeline writes from, highest `priority` first.

While the pipeline is on, a planned topic is written (and spends credits) at the next tick,
within the batch size and caps the person already approved when they turned the pipeline on.
While it is off, nothing happens until it is turned on or a run is requested. To keep a topic
without it being written, add it and then set it to `skipped`.

`format_key` must be the key of an active format this Site can use (`GET /formats`), or it
is `422`.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped to the Site and endpoint and kept for 24 hours. - Same key and body: the original response is replayed, so retrying a timeout is safe. - Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`. - Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. |

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `topic_or_keyword` | string | yes |  |
| `priority` | integer | no | Higher is written first. |
| `target_publish_date` | date | null | no |  |
| `format_key` | string | null | no | The `key` of an active format this Site can use (`GET /formats`). |

**Responses**

| Status | Data | Description |
|---|---|---|
| 201 | PlanItem | The new plan item. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/queue \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "topic_or_keyword": "<topic_or_keyword>"
}'
```

### GET /pipeline/sources

List competitor source articles

- **Operation id**: `listPipelineSources`
- **Scope**: `pipeline:read`
- **Permission**: `pipeline.run`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The source queue, newest first. When the Site has competitors with a blog URL, the engine
finds their articles (`discovered`), scores each one on its search traffic, ranking keywords
and links (`scored`, or `skipped` below the Site's `source_score_threshold`), reads it
(`scraped`, or `rejected` / `failed`), and then writes an original article that beats it
(`used`, with `article_id` set).

Metadata and scores only. The text read from the competitor page is never returned by
this API.

A metric that could not be measured is `null`, never `0`.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `cursor` | query | string | no | The opaque cursor from `data.next_cursor` on the previous page. Do not parse or build one; its encoding will change. |
| `limit` | query | integer | no | Page size. Values above the maximum are clamped, not rejected: ask for a thousand and you get a hundred and a `next_cursor`. |
| `status` | query | PipelineSourceStatus | no |  |
| `competitor_id` | query | uuid | no | Only sources found on this competitor's blog. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Page | A page of source articles. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/sources \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/sources/{id}

Read one competitor source article

- **Operation id**: `getPipelineSource`
- **Scope**: `pipeline:read`
- **Permission**: `pipeline.run`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

One source article with its score breakdown, where it is in the queue, and the article it
produced once written. The text read from the competitor page is never returned.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | PipelineSource | The source article. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/sources/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/config

Read the pipeline configuration

- **Operation id**: `getPipelineConfig`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Whether the AI pipeline runs on its own for this Site, and how hard: the publish mode, the daily
publish limit, the batch size, the rewrite attempts and the quality threshold, the competitor
source pass mark and its weights and the minimum originality, plus when it next runs and whether
a run is in flight. Read this before `PATCH /pipeline/config`.

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | PipelineConfig | The pipeline configuration. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/config \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### PATCH /pipeline/config

Change the pipeline configuration

- **Operation id**: `updatePipelineConfig`
- **Scope**: `pipeline:config`
- **Permission**: `pipeline.config`
- **Rate limit class**: write
- **Plan feature**: `ai.article_generation`
- **Spends credits**: yes
- **AI agent approval**: `pipeline.configure`, always asked. Asked only when the result is a running pipeline that does more than before: turning it on, raising `batch_size`, `articles_per_day`, `max_auto_improve_attempts`, `quality_score_threshold` or `min_originality_pct`, lowering `source_score_threshold`, changing `seo_score_weights`, or switching `publish_mode` to `auto`. Turning it off is never asked.
- **Publishable key may call it**: no

Turn the AI pipeline on or off, or change how much it does. Send at least one field. While the
pipeline is on it writes articles from the content plan every 10 minutes, up to `batch_size` at a
time, and every article spends credits; in `auto` mode finished articles go live on the blog
without review, up to `articles_per_day` a day.

Turning it on checks the plan first: a plan without AI generation is `402 NOT_ENTITLED` rather
than a switch that silently does nothing. Turning it off, or any change while it stays off, is
never gated and never spends anything.

The response carries `warnings`. Turning the pipeline on while the AI writing profile has gaps
(see `gaps` on `GET /site/knowledge-profile`) still turns it on, and adds a
`KNOWLEDGE_PROFILE_INCOMPLETE` warning: until the profile is filled in, articles are written
with generic placeholders instead of the brand's details.

**These settings are a shortcut into the article recipe** (see `GET /pipeline/recipe`). Since
openapi 1.4.0 the recipe is what the pipeline follows: every field here except
`pipeline_enabled` and `batch_size` is written as a new live recipe version (the equivalent of a
draft switched on), which is refused with `409 CONFLICT` while a recipe draft is open. A field
the Site's recipe cannot carry is `422` (for example `source_score_threshold` when its ideas do
not come from competitor blogs).

The three competitor source knobs: `source_score_threshold` (0-100) is the SEO score a
competitor article must reach before it is scraped and rewritten, `seo_score_weights` says how
much each metric counts toward that score, and `min_originality_pct` (0-100) is the share of
5-word phrases that must not be copied from the competitor article before an article can
publish.

Send `If-Match` with the `ETag` from your last read.

**When an AI agent asks.** A key from a device or OAuth sign-in, or any request from an MCP
client, always gets `428 APPROVAL_REQUIRED` on the first try, whatever the organisation's
approvals switch says, because it spends money or credits. Asked only when the result is a
running pipeline that does more than before: turning it on, raising `batch_size`,
`articles_per_day`, `max_auto_improve_attempts`, `quality_score_threshold` or
`min_originality_pct`, lowering `source_score_threshold`, changing `seo_score_weights`, or
switching `publish_mode` to `auto`. Turning it off is never asked. Give the person
`error.approval.url`; once they approve, repeat the identical request with `Writavo-Approval:
<error.approval.id>`. The approval is void if what the request would do changes before the
retry. A key made in the dashboard is asked only when the request comes from an MCP client.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `If-Match` | header | string | no | The `ETag` from your last read. If the object changed since, you get `412 PRECONDITION_FAILED` and nothing is written. Optional in v1; without it the last write wins. Send it. |
| `Writavo-Approval` | header | uuid | no | The `error.approval.id` from a `428 APPROVAL_REQUIRED`, sent on the retry once a person has approved it. Only meaningful for a key approved for an AI agent; every other caller can leave it out. It is single use and bound to the key, to the exact request that asked for it (method, path and body) and to what that request would do: anything else, including a request whose effect changed since it was approved, is `409 APPROVAL_INVALID`. |

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `pipeline_enabled` | boolean | no |  |
| `publish_mode` | string | no |  |
| `articles_per_day` | integer | no |  |
| `batch_size` | integer | no |  |
| `max_auto_improve_attempts` | integer | no |  |
| `quality_score_threshold` | number | no |  |
| `source_score_threshold` | number | no |  |
| `seo_score_weights` | SeoScoreWeights | no | How much each metric counts toward a competitor article's SEO score. The weights are relative (the score is normalised over the metrics that were measured), so they need not add up to 1. Send all five; at least one must be above 0. |
| `min_originality_pct` | number | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | PipelineConfig | The updated configuration. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 402 | - | `NOT_ENTITLED`, `INSUFFICIENT_CREDITS` or `SPEND_CAP_REACHED`: allowed, but it cannot be paid for. The fix is, in order: upgrade the plan, top up credits, or raise the Site's cap. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 412 | - | `PRECONDITION_FAILED`. Your `If-Match` no longer matches: someone edited the object. Nothing was written. Re-read, merge, and retry with the new `ETag`. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 428 | - | `APPROVAL_REQUIRED` or `APPROVAL_PENDING`. Only ever returned to an AI agent: a key from a device or OAuth sign-in, or any request an MCP client makes (it carries `Writavo-Mcp-Tool`) whatever key it holds. For an operation marked `x-writavo-approval-always` it is returned whatever the organisation's approvals switch says, and for the six content actions while approvals are on. Nothing has happened yet. Show `error.approval.url` to a person; once they approve, repeat the identical request with `Writavo-Approval: <error.approval.id>`. Do not change the body or the path: the approval is bound to the exact request, and a different one is `409 APPROVAL_INVALID`. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X PATCH https://api.writavo.com/v1/pipeline/config \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H 'If-Match: W/"1767225600000"' \
  -H "Content-Type: application/json" \
  -d '{}'
```

### GET /pipeline/steps

List the steps a recipe can use

- **Operation id**: `listRecipeSteps`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The step catalogue for article recipes. A recipe is the Site's pipeline as data: steps in seven
fixed sections (where ideas come from, which ideas to keep, research, writing, checks, finishing
touches, publishing). Every step here is one Writavo built and priced; there is no custom code.

Each step lists its section, what it works with (`produces` for an ideas step, `accepts` for the
rest, `only_with_ideas` / `only_with_writers` where it needs a particular partner), its settings
(type, range, default, a plain label) and what one run costs in credits. `locked` steps are
added by Writavo wherever they apply and cannot be removed. A step whose `status` is
`coming_soon` cannot be used yet.

Read this before writing a recipe with `POST /pipeline/recipe/draft`.

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | The step catalogue. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/steps \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/templates

List the starting recipes

- **Operation id**: `listRecipeTemplates`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

One ready-made recipe per goal ("Win traffic from competitors", "Cover the news in my niche",
"Write the topics I plan", "Build pages from a list", "Keep old articles fresh", "Let AI pick my
topics"), each with its spec, its plain sentences and what it costs per article. `needs` lists
what a person must fill in before the template can be saved (news feed addresses, a CSV link).

To start from one, send `{"template_key": "..."}` (or the template's `spec`, edited) to
`POST /pipeline/recipe/draft`.

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | The templates. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/templates \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/recipe

Read the Site's article recipe

- **Operation id**: `getRecipe`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The recipe that is running (`live`), the draft being edited (`draft`, or null) and the last ten
versions. Each version carries its `spec`, the plain `sentences` the dashboard shows, advice
(`warnings`) and an `estimate` of what it costs per article and per month (based on the Site's
own last 14 days once it has that much history).

Articles already on their way finish on the recipe version they started on, so switching on a
new recipe never changes work in flight.

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Recipe | The recipe. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/recipe \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/recipe/explain

Explain the recipe in plain words

- **Operation id**: `explainRecipe`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The recipe as the sentences a person sees on the recipe page, one per step, in order, for
example "Every 10 minutes, check 3 news feeds". Use these words when you tell a person what
their recipe does.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `version` | query | string | no | Which version to explain. Default `live`. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | The sentences. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/recipe/explain \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### GET /pipeline/recipe/activity

See what the recipe did, step by step

- **Operation id**: `listRecipeActivity`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The most recent ideas and articles, each with its step-by-step trace (found, kept or dropped
and why, read, written, checked, illustrated, published) and the credits each step charged.
Pass `item` (an article, source or topic id) for one item's full trace. Test runs are marked
`is_test`.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | integer | no | Page size. Values above the maximum are clamped, not rejected: ask for a thousand and you get a hundred and a `next_cursor`. |
| `item` | query | uuid | no | One article, source or topic id. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | Recent items with their traces. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/recipe/activity \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### POST /pipeline/recipe/estimate

Estimate what a recipe costs

- **Operation id**: `estimateRecipe`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Credits per article and per month for a spec (send `spec`, validated as for a draft) or for the
Site's `live` or `draft` recipe (send `version`). `per_day` overrides how many articles a day
the monthly figure assumes. Nothing is saved and nothing is charged.

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `spec` | RecipeSpec | no | An article recipe (spec_version 1): one step for where ideas come from, zero or more to keep, zero or more research steps, one writer, the checks, the finishing touches and one publishing step. Every entry is `{step, settings}`. |
| `version` | string | no |  |
| `per_day` | number | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | RecipeEstimate | The estimate. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/recipe/estimate \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### POST /pipeline/recipe/draft

Save the draft recipe

- **Operation id**: `saveRecipeDraft`
- **Scope**: `pipeline:config`
- **Permission**: `pipeline.config`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Save (or replace) the Site's draft recipe. Send `spec`, or `template_key` to start from a
template. Nothing runs until the draft is switched on with `POST /pipeline/recipe/draft/activate`,
so saving a draft never spends anything.

The spec is validated against the step catalogue: every section's rules, every setting's type
and range, and the combinations (for example, a recipe that publishes on its own needs a quality
check). A problem is `422 VALIDATION_FAILED` with `error.fields` keyed by the spec path, such as
`spec.keep[1].settings.min_score`. The saved spec is normalised: defaults filled in and locked
steps (reading the source, originality, category and tags) added where they apply.

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `spec` | RecipeSpec | no | An article recipe (spec_version 1): one step for where ideas come from, zero or more to keep, zero or more research steps, one writer, the checks, the finishing touches and one publishing step. Every entry is `{step, settings}`. |
| `template_key` | string | no | A key from `GET /pipeline/templates`. |
| `note` | string | null | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | The saved draft. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/recipe/draft \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### DELETE /pipeline/recipe/draft

Discard the draft recipe

- **Operation id**: `discardRecipeDraft`
- **Scope**: `pipeline:config`
- **Permission**: `pipeline.config`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Throw the draft away. The live recipe is not touched.

**Responses**

| Status | Data | Description |
|---|---|---|
| 204 | - | Discarded. No body. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X DELETE https://api.writavo.com/v1/pipeline/recipe/draft \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### POST /pipeline/recipe/draft/test

Test the draft on one real idea

- **Operation id**: `testRecipeDraft`
- **Scope**: `pipeline:run`
- **Permission**: `pipeline.run`
- **Rate limit class**: pipeline
- **Plan feature**: `ai.article_generation`
- **Spends credits**: yes
- **AI agent approval**: `pipeline.run`, always asked
- **Publishable key may call it**: no

**This spends credits.** Takes ONE real idea through every step of the draft recipe and stops at
the review queue. The test article is never published. It returns `202` straight away; follow the
test with `GET /pipeline/recipe/activity?item=<item_id>`.

The idea: for news feeds, the newest story from the recipe's feeds; for competitor blogs, the
most recently found post (or pass `url`); for topics, `topic` (or the top planned topic); for a
refresh, `article_id` (or the oldest published article). If a keep step drops the idea, the test
ends there and the trace says why, which is itself useful.

**When an AI agent asks.** Always `428 APPROVAL_REQUIRED` on the first try: it spends credits.
Retry the identical request with `Writavo-Approval: <error.approval.id>` once a person approves.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped to the Site and endpoint and kept for 24 hours. - Same key and body: the original response is replayed, so retrying a timeout is safe. - Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`. - Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. |
| `Writavo-Approval` | header | uuid | no | The `error.approval.id` from a `428 APPROVAL_REQUIRED`, sent on the retry once a person has approved it. Only meaningful for a key approved for an AI agent; every other caller can leave it out. It is single use and bound to the key, to the exact request that asked for it (method, path and body) and to what that request would do: anything else, including a request whose effect changed since it was approved, is `409 APPROVAL_INVALID`. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `topic` | string | no |  |
| `url` | uri | no |  |
| `article_id` | uuid | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 202 | object | The test started. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 402 | - | `NOT_ENTITLED`, `INSUFFICIENT_CREDITS` or `SPEND_CAP_REACHED`: allowed, but it cannot be paid for. The fix is, in order: upgrade the plan, top up credits, or raise the Site's cap. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 428 | - | `APPROVAL_REQUIRED` or `APPROVAL_PENDING`. Only ever returned to an AI agent: a key from a device or OAuth sign-in, or any request an MCP client makes (it carries `Writavo-Mcp-Tool`) whatever key it holds. For an operation marked `x-writavo-approval-always` it is returned whatever the organisation's approvals switch says, and for the six content actions while approvals are on. Nothing has happened yet. Show `error.approval.url` to a person; once they approve, repeat the identical request with `Writavo-Approval: <error.approval.id>`. Do not change the body or the path: the approval is bound to the exact request, and a different one is `409 APPROVAL_INVALID`. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/recipe/draft/test \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### POST /pipeline/recipe/draft/activate

Switch the draft recipe on

- **Operation id**: `activateRecipeDraft`
- **Scope**: `pipeline:config`
- **Permission**: `pipeline.config`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: yes
- **AI agent approval**: `pipeline.configure`, always asked
- **Publishable key may call it**: no

The draft becomes the live recipe; the previous one is kept in the history and
`POST /pipeline/recipe/undo` goes back to it. New ideas follow the new recipe at once; articles
already on their way finish on the version they started on. Returns the recipe as
`GET /pipeline/recipe` does, plus `warnings`: a `KNOWLEDGE_PROFILE_INCOMPLETE` entry when the AI
writing profile has gaps (the recipe is switched on either way).

**When an AI agent asks.** Always `428 APPROVAL_REQUIRED` on the first try, whatever the
organisation's approvals switch says: a recipe decides what the pipeline spends. The approval
page shows the new recipe's cost per article next to the current one. The approval is bound to
the draft as it is: an edit after the person approved voids it.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped to the Site and endpoint and kept for 24 hours. - Same key and body: the original response is replayed, so retrying a timeout is safe. - Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`. - Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. |
| `Writavo-Approval` | header | uuid | no | The `error.approval.id` from a `428 APPROVAL_REQUIRED`, sent on the retry once a person has approved it. Only meaningful for a key approved for an AI agent; every other caller can leave it out. It is single use and bound to the key, to the exact request that asked for it (method, path and body) and to what that request would do: anything else, including a request whose effect changed since it was approved, is `409 APPROVAL_INVALID`. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Recipe | The recipe, with the new version live. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 428 | - | `APPROVAL_REQUIRED` or `APPROVAL_PENDING`. Only ever returned to an AI agent: a key from a device or OAuth sign-in, or any request an MCP client makes (it carries `Writavo-Mcp-Tool`) whatever key it holds. For an operation marked `x-writavo-approval-always` it is returned whatever the organisation's approvals switch says, and for the six content actions while approvals are on. Nothing has happened yet. Show `error.approval.url` to a person; once they approve, repeat the identical request with `Writavo-Approval: <error.approval.id>`. Do not change the body or the path: the approval is bound to the exact request, and a different one is `409 APPROVAL_INVALID`. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/recipe/draft/activate \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)"
```

### POST /pipeline/recipe/undo

Go back to the previous recipe

- **Operation id**: `undoRecipe`
- **Scope**: `pipeline:config`
- **Permission**: `pipeline.config`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: yes
- **AI agent approval**: `pipeline.configure`, always asked. Asked only when the previous recipe costs more per article than the current one.
- **Publishable key may call it**: no

Switch the previous recipe back on, as a new version (the history stays in order). Refused with
`409 CONFLICT` while a draft is open.

**When an AI agent asks.** `428 APPROVAL_REQUIRED` only when the previous recipe costs more per
article than the current one.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped to the Site and endpoint and kept for 24 hours. - Same key and body: the original response is replayed, so retrying a timeout is safe. - Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`. - Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. |
| `Writavo-Approval` | header | uuid | no | The `error.approval.id` from a `428 APPROVAL_REQUIRED`, sent on the retry once a person has approved it. Only meaningful for a key approved for an AI agent; every other caller can leave it out. It is single use and bound to the key, to the exact request that asked for it (method, path and body) and to what that request would do: anything else, including a request whose effect changed since it was approved, is `409 APPROVAL_INVALID`. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Recipe | The recipe, with the restored version live. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 428 | - | `APPROVAL_REQUIRED` or `APPROVAL_PENDING`. Only ever returned to an AI agent: a key from a device or OAuth sign-in, or any request an MCP client makes (it carries `Writavo-Mcp-Tool`) whatever key it holds. For an operation marked `x-writavo-approval-always` it is returned whatever the organisation's approvals switch says, and for the six content actions while approvals are on. Nothing has happened yet. Show `error.approval.url` to a person; once they approve, repeat the identical request with `Writavo-Approval: <error.approval.id>`. Do not change the body or the path: the approval is bound to the exact request, and a different one is `409 APPROVAL_INVALID`. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/recipe/undo \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)"
```

### POST /pipeline/queue/reorder

Reorder the content plan

- **Operation id**: `reorderPlanItems`
- **Scope**: `plan:write`
- **Permission**: `articles.write`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Move up to 100 planned or parked topics to the top of the plan, in the order given. The first id
becomes the highest priority. Topics you do not name keep their order below them.

Every id must be a `planned` or `skipped` item on this Site; the first unknown one is `404`.
Returns the named items in their new order.

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | uuid[] | yes | Plan item ids, highest priority first. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | The reordered items. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/queue/reorder \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": "3f1b0c7a-0000-4000-8000-000000000001"
}'
```

### GET /pipeline/queue/{id}

Read one content plan item

- **Operation id**: `getPlanItem`
- **Scope**: `pipeline:read`
- **Permission**: `none`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

One topic in the content plan, with its status, priority, format and the article it produced
once written.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | PlanItem | The plan item. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl https://api.writavo.com/v1/pipeline/queue/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### PATCH /pipeline/queue/{id}

Change a content plan item

- **Operation id**: `updatePlanItem`
- **Scope**: `plan:write`
- **Permission**: `articles.write`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Change a topic's wording, priority, target date or format, or park it (`status: skipped`) and
bring it back (`status: planned`). The pipeline only ever takes `planned` items.

Only while the item is `planned` or `skipped`: one that is being written or already written is
`409 CONFLICT` ("This topic has already been written.").

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `topic_or_keyword` | string | no |  |
| `priority` | integer | no |  |
| `target_publish_date` | date | null | no |  |
| `format_key` | string | null | no | The `key` of an active format this Site can use (`GET /formats`). |
| `status` | string | no | `skipped` parks the topic; the pipeline only takes `planned` ones. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | PlanItem | The updated item. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X PATCH https://api.writavo.com/v1/pipeline/queue/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### DELETE /pipeline/queue/{id}

Remove a content plan item

- **Operation id**: `deletePlanItem`
- **Scope**: `plan:write`
- **Permission**: `articles.write`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Remove a topic from the plan. Only while it is `planned` or `skipped`; a topic being written or
already written is `409 CONFLICT`. Articles are never touched.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 204 | - | Removed. No body. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X DELETE https://api.writavo.com/v1/pipeline/queue/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```

### POST /pipeline/articles/{id}/requeue

Send an article back through the pipeline

- **Operation id**: `requeueArticle`
- **Scope**: `pipeline:run`
- **Permission**: `pipeline.run`
- **Rate limit class**: pipeline
- **Plan feature**: `ai.article_generation`
- **Spends credits**: yes
- **AI agent approval**: `article.requeue`, always asked. Asked only for `to: improve` and `to: images`; `to: publish` is never asked.
- **Publishable key may call it**: no

Send a pipeline article back to a stage: `improve` (an AI rewrite, spends credits), `images` (new
images, spends credits) or `publish` (queue it for its publishing slot, free). This is how a
rejected, failed or reviewed article is moved on without the dashboard.

| `to` | Allowed from | Becomes | Spends |
|---|---|---|---|
| `improve` | `generated`, `needs_improvement`, `rejected`, `failed` | `needs_improvement`, rewrite attempts reset | credits |
| `images` | `generated`, `needs_improvement`, `needs_images`, `rejected`, `failed` (with content) | `needs_images` | credits |
| `publish` | `generated`, `needs_improvement`, `needs_images`, `rejected` | `queued` | nothing |

Any other status is `409 CONFLICT` naming it; a hand-written `draft` is published with
`POST /articles/{id}/publish` instead. A `queued` article goes live at the next publishing slot
while the pipeline is in `auto` mode, and waits for a person in `review` mode.

`improve` needs the plan's AI generation and `images` its image generation (`402 NOT_ENTITLED`
otherwise), and both check the organisation's status and the Site's daily spend cap
(`402 SPEND_CAP_REACHED`) first. Credits are charged by the stage as it does the work.

**When an AI agent asks.** A key from a device or OAuth sign-in, or any request from an MCP
client, always gets `428 APPROVAL_REQUIRED` on the first try, whatever the organisation's
approvals switch says, because it spends money or credits. Asked only for `to: improve` and `to:
images`; `to: publish` is never asked. Give the person `error.approval.url`; once they approve,
repeat the identical request with `Writavo-Approval: <error.approval.id>`. The approval is void
if what the request would do changes before the retry. A key made in the dashboard is asked only
when the request comes from an MCP client.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |
| `Idempotency-Key` | header | string | yes | A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped to the Site and endpoint and kept for 24 hours. - Same key and body: the original response is replayed, so retrying a timeout is safe. - Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`. - Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. |
| `Writavo-Approval` | header | uuid | no | The `error.approval.id` from a `428 APPROVAL_REQUIRED`, sent on the retry once a person has approved it. Only meaningful for a key approved for an AI agent; every other caller can leave it out. It is single use and bound to the key, to the exact request that asked for it (method, path and body) and to what that request would do: anything else, including a request whose effect changed since it was approved, is `409 APPROVAL_INVALID`. |

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `to` | string | yes | `improve` and `images` spend credits; `publish` is free. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | ArticleLifecycleState | The article's new state. |
| 400 | - | `INVALID_REQUEST`. Bad JSON, an unusable query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`: no key, or an unusable one. It never distinguishes "no such key" from "wrong key". |
| 402 | - | `NOT_ENTITLED`, `INSUFFICIENT_CREDITS` or `SPEND_CAP_REACHED`: allowed, but it cannot be paid for. The fix is, in order: upgrade the plan, top up credits, or raise the Site's cap. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key lacks this operation's scope, or its creator's permissions no longer cover it. Another Site's object is always 404, never this. A 403 may also be: - `AGENT_ACCESS_DISABLED` - AI agent access is turned off. - `APPROVAL_DENIED` - a person denied this action. - `FORBIDDEN` (organisation, team, settings) - a change no scope allows, such as an AI agent changing the access of the person it acts for, anything about ownership, or a setting only a person may change. The message says what to do instead. |
| 404 | - | `NOT_FOUND`. No such object, or it belongs to another Site. **The two are deliberately indistinguishable and never return 403.** If the id is right, check you are using the right Site's key. |
| 409 | - | The request collides with the current state. Read `code`: - `SLUG_CONFLICT` - the slug is taken. - `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT` - key reused with another body, or still running. - `CONFLICT` - the object changed under you. Re-read and retry. - `APPROVAL_INVALID` - the `Writavo-Approval` is expired, used, unknown, for another request, or its effect changed. Retry without it for a new approval. - `PREREQUISITE_MISSING` / `FEATURE_UNAVAILABLE` (settings, delivery, SEO) - something is not set up (the message names what sets it up), or Writavo has it switched off (the message names the free alternative). Nothing was done or charged. |
| 422 | - | `VALIDATION_FAILED`. The values are not acceptable; `error.fields` maps each field to a message. Common causes: publishing without a title, slug or content; scheduling in the past; sending `status` on a create or update. |
| 428 | - | `APPROVAL_REQUIRED` or `APPROVAL_PENDING`. Only ever returned to an AI agent: a key from a device or OAuth sign-in, or any request an MCP client makes (it carries `Writavo-Mcp-Tool`) whatever key it holds. For an operation marked `x-writavo-approval-always` it is returned whatever the organisation's approvals switch says, and for the six content actions while approvals are on. Nothing has happened yet. Show `error.approval.url` to a person; once they approve, repeat the identical request with `Writavo-Approval: <error.approval.id>`. Do not change the body or the path: the approval is bound to the exact request, and a different one is `409 APPROVAL_INVALID`. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. The two device sign-in endpoints take no key, so they are limited per IP address. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys, webhooks, the content plan, formats and prompts, settings, delivery, SEO curation, the team and billing \| 120 per minute \| \| upload \| `POST /media/upload-url` and `POST /seo/backlinks/import` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs`, `POST /pipeline/articles/{id}/requeue`, `POST /delivery/cms/push` and the seven paid SEO scans \| 10 per minute \| \| auth \| `POST /auth/device` and `POST /auth/device/token`, per IP address \| 60 per minute \| `POST /comments` adds three limits, named in `error.limit` on its 429: `reader` (5 per reader in 10 minutes, by `reader_ip`), `site` (300 per Site per hour) and `queue` (2,000 awaiting review). Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 500 | - | `INTERNAL_ERROR`. Our fault. Quote the `request_id` from the body if you contact support. Safe to retry; send the same `Idempotency-Key` so a request that did succeed is not repeated. |
| 503 | - | `MAINTENANCE`. Writes are paused, platform wide or for your Site. Reads usually work and your published blog stays up. Retry after `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/pipeline/articles/3f1b0c7a-0000-4000-8000-000000000001/requeue \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "improve"
}'
```
