# Webhooks

Outbound event delivery. Register an HTTPS endpoint, subscribe it to events, and receive a
signed POST whenever your content changes, from this API or from the dashboard.

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

### GET /webhooks

List webhook endpoints

- **Operation id**: `listWebhooks`
- **Scope**: `webhooks:read`
- **Permission**: `integrations.manage`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `cursor` | query | string | no | The opaque cursor from `data.next_cursor` on the previous page. Do not parse it or construct one; its encoding is not part of this contract and will change. |
| `limit` | query | integer | no | Page size. Values above the maximum are clamped rather than rejected, so a client asking for a thousand rows gets a hundred and a `next_cursor`. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Page | A page of webhook endpoints. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

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

### POST /webhooks

Create a webhook endpoint

- **Operation id**: `createWebhook`
- **Scope**: `webhooks:write`
- **Permission**: `integrations.manage`
- **Rate limit class**: write
- **Plan feature**: `cms.webhooks`
- **Spends credits**: no
- **Publishable key may call it**: no

Registers a URL to receive signed event deliveries. The signing secret is returned once,
at creation, and never again: record it before you close the response. If you lose it,
`POST /webhooks/{id}/rotate-secret` issues a new one.

Verify every delivery, using the recipe at the top of this section, before you parse the
body.

The URL must be public HTTPS. Private, loopback, link-local and metadata addresses are
refused, and so is a hostname that resolves to one, at save time and again at delivery
time. A webhook cannot be pointed at internal infrastructure.

Delivery is at least once. Retries use exponential backoff, and an endpoint that keeps
failing is disabled automatically and reported to you. Deduplicate on the payload's `id`
and make your handler idempotent.

A Site may register at most ten endpoints.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice. Retry with the **same** key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out. Same key with a **different** body is `409 IDEMPOTENCY_KEY_CONFLICT`, because reusing a key for different work is a bug in your client rather than a retry. Same key while the first request is still running is `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation. |

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | uri | yes |  |
| `events` | WebhookEvent[] | yes |  |
| `description` | string | null | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 201 | Webhook | The endpoint was created. The signing secret is returned once. |
| 400 | - | `INVALID_REQUEST`. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 402 | - | `NOT_ENTITLED`, `INSUFFICIENT_CREDITS` or `SPEND_CAP_REACHED`. The request was understood and you were permitted to make it, but it cannot be paid for. Three distinct codes because the fixes differ: upgrade the plan, top up credits, or raise the Site's cap. These are 402 rather than 403 on purpose. A 403 says "you may not"; these all say "not yet, and here is how to change that". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 409 | - | `SLUG_CONFLICT`, `IDEMPOTENCY_KEY_CONFLICT`, `IDEMPOTENCY_KEY_IN_FLIGHT` or `CONFLICT`. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Read `code` to tell which, then re-read and retry. |
| 422 | - | `VALIDATION_FAILED`. The request parsed but the values are not acceptable. `error.fields` maps each offending field to a message you can put next to the input. Common causes: publishing without a title, slug or content; scheduling in the past; and sending `status` on a create or update, which is how the API refuses to let a client push content into the AI pipeline. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

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

### GET /webhooks/{id}

Read one webhook endpoint

- **Operation id**: `getWebhook`
- **Scope**: `webhooks:read`
- **Permission**: `integrations.manage`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

**Parameters**

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

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Webhook | The webhook endpoint. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 404 | - | `NOT_FOUND`. Either no such object exists, or it exists and belongs to a different Site. **These two cases are deliberately indistinguishable, and neither ever returns 403.** A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

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

### PATCH /webhooks/{id}

Update a webhook endpoint

- **Operation id**: `updateWebhook`
- **Scope**: `webhooks:write`
- **Permission**: `integrations.manage`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Change the URL, the subscribed events, or re-enable an endpoint that was auto-disabled
after repeated failures. Re-enabling does not replay what you missed.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |
| `If-Match` | header | string | no | The `ETag` from your last read of this object. If it has changed since then you get `412 PRECONDITION_FAILED` and your write is not applied, so two people editing the same article cannot silently overwrite each other. Optional in v1 for compatibility. Omitting it means last write wins. Send it. |

**Request body** (required)

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | uri | no |  |
| `events` | WebhookEvent[] | no |  |
| `description` | string | null | no |  |
| `enabled` | boolean | no |  |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Webhook | The updated webhook endpoint. |
| 400 | - | `INVALID_REQUEST`. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 404 | - | `NOT_FOUND`. Either no such object exists, or it exists and belongs to a different Site. **These two cases are deliberately indistinguishable, and neither ever returns 403.** A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site. |
| 412 | - | `PRECONDITION_FAILED`. Your `If-Match` did not match the current version, meaning somebody edited the object since you read it. Nothing was written. Re-read, merge, and retry with the new `ETag`. |
| 422 | - | `VALIDATION_FAILED`. The request parsed but the values are not acceptable. `error.fields` maps each offending field to a message you can put next to the input. Common causes: publishing without a title, slug or content; scheduling in the past; and sending `status` on a create or update, which is how the API refuses to let a client push content into the AI pipeline. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

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

### DELETE /webhooks/{id}

Delete a webhook endpoint

- **Operation id**: `deleteWebhook`
- **Scope**: `webhooks:write`
- **Permission**: `integrations.manage`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Deliveries stop at once. Queued retries for this endpoint are dropped.

**Parameters**

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

**Responses**

| Status | Data | Description |
|---|---|---|
| 204 | - | Deleted. No body. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 404 | - | `NOT_FOUND`. Either no such object exists, or it exists and belongs to a different Site. **These two cases are deliberately indistinguishable, and neither ever returns 403.** A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

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

### POST /webhooks/{id}/rotate-secret

Rotate the signing secret

- **Operation id**: `rotateWebhookSecret`
- **Scope**: `webhooks:write`
- **Permission**: `integrations.manage`
- **Rate limit class**: write
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

Issues a new signing secret and returns it once. The old secret stops working
immediately: there is no grace window, because a window in which two secrets both
validate is a window in which a leaked secret still works.

Deliveries sent between the rotation and your receiver being updated will fail
verification. They are retried on the normal backoff, so update the receiver promptly and
the queue drains itself.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |
| `Idempotency-Key` | header | string | yes | A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice. Retry with the **same** key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out. Same key with a **different** body is `409 IDEMPOTENCY_KEY_CONFLICT`, because reusing a key for different work is a bug in your client rather than a retry. Same key while the first request is still running is `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry. Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | object | The new signing secret. Shown once. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 404 | - | `NOT_FOUND`. Either no such object exists, or it exists and belongs to a different Site. **These two cases are deliberately indistinguishable, and neither ever returns 403.** A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site. |
| 409 | - | `SLUG_CONFLICT`, `IDEMPOTENCY_KEY_CONFLICT`, `IDEMPOTENCY_KEY_IN_FLIGHT` or `CONFLICT`. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Read `code` to tell which, then re-read and retry. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/webhooks/3f1b0c7a-0000-4000-8000-000000000001/rotate-secret \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)"
```

### GET /webhooks/{id}/deliveries

List deliveries for an endpoint

- **Operation id**: `listWebhookDeliveries`
- **Scope**: `webhooks:read`
- **Permission**: `integrations.manage`
- **Rate limit class**: read
- **Plan feature**: `none`
- **Spends credits**: no
- **Publishable key may call it**: no

The delivery log, newest first. Use it to answer "did you send it and did we accept it".
`response_status` is what your server returned; `attempt` counts from 1 to 6.

There is one row per ATTEMPT, and every attempt of one event shares its `event_id`. A row
with `status: failed` and `exhausted: false` will be retried; `exhausted: true` means the
ladder gave up.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | uuid | yes |  |
| `cursor` | query | string | no | The opaque cursor from `data.next_cursor` on the previous page. Do not parse it or construct one; its encoding is not part of this contract and will change. |
| `limit` | query | integer | no | Page size. Values above the maximum are clamped rather than rejected, so a client asking for a thousand rows gets a hundred and a `next_cursor`. |
| `status` | query | string | no |  |
| `event` | query | WebhookEvent | no | Return only attempts for this event type. |

**Responses**

| Status | Data | Description |
|---|---|---|
| 200 | Page | A page of deliveries. |
| 400 | - | `INVALID_REQUEST`. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 404 | - | `NOT_FOUND`. Either no such object exists, or it exists and belongs to a different Site. **These two cases are deliberately indistinguishable, and neither ever returns 403.** A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

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

### POST /webhooks/{id}/deliveries/{delivery_id}/redeliver

Send one delivery again

- **Operation id**: `redeliverWebhookDelivery`
- **Scope**: `webhooks:write`
- **Permission**: `integrations.manage`
- **Rate limit class**: write
- **Plan feature**: `cms.webhooks`
- **Spends credits**: no
- **Publishable key may call it**: no

Queues the same event for delivery again, with the same `event_id` and byte-identical
body, so your handler sees it as the event it already knows how to deduplicate rather
than as a second, different fact. Use it after fixing a receiver that was down.

The attempt counter starts again at 1: this is a fresh ladder you asked for, not a
continuation of the one that failed. The endpoint must be enabled.

**Parameters**

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

**Responses**

| Status | Data | Description |
|---|---|---|
| 202 | object | Queued. It will be attempted within the minute. |
| 401 | - | `INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. |
| 402 | - | `NOT_ENTITLED`, `INSUFFICIENT_CREDITS` or `SPEND_CAP_REACHED`. The request was understood and you were permitted to make it, but it cannot be paid for. Three distinct codes because the fixes differ: upgrade the plan, top up credits, or raise the Site's cap. These are 402 rather than 403 on purpose. A 403 says "you may not"; these all say "not yet, and here is how to change that". |
| 403 | - | `INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it. This is the **only** 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists. |
| 404 | - | `NOT_FOUND`. Either no such object exists, or it exists and belongs to a different Site. **These two cases are deliberately indistinguishable, and neither ever returns 403.** A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site. |
| 409 | - | `SLUG_CONFLICT`, `IDEMPOTENCY_KEY_CONFLICT`, `IDEMPOTENCY_KEY_IN_FLIGHT` or `CONFLICT`. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Read `code` to tell which, then re-read and retry. |
| 429 | - | `RATE_LIMIT_EXCEEDED`. Back off and honour `Retry-After`. Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money. \| Class \| Endpoints \| Limit \| \|---\|---\|---\| \| read \| every GET \| 600 per minute \| \| write \| POST, PATCH and DELETE on content, taxonomy, keys and webhooks \| 120 per minute \| \| upload \| `POST /media/upload-url` \| 60 per minute \| \| pipeline \| `POST /pipeline/runs` \| 10 per minute \| Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so you can slow down before you are refused rather than after. |
| 500 | - | `INTERNAL_ERROR`. Something failed on our side. The message is deliberately generic; the detail is in our logs against the `request_id` in the body, so quote it if you contact support. Safe to retry, and safer still with the same `Idempotency-Key`, which guarantees you do not create a second object if the first request actually succeeded. |
| 503 | - | `MAINTENANCE`. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in `Retry-After`. |

```bash
curl -X POST https://api.writavo.com/v1/webhooks/3f1b0c7a-0000-4000-8000-000000000001/deliveries/3f1b0c7a-0000-4000-8000-000000000001/redeliver \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"
```
