Documentation menu

reference

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.

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.

GET/webhooks

List webhook endpoints#

Scope
webhooks:read
Rate limit
600/min (read)
Publishable key
refused
Dashboard permission
integrations.manage

Parameters

NameInTypeNotes
cursorquerystring

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.

limitqueryinteger

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.

default 20

Responses

  • 200a page of Webhook

    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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

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

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

POST/webhooks

Create a webhook endpoint#

Scope
webhooks:write
Rate limit
120/min (write)
Publishable key
refused
Plan feature
cms.webhooks
Dashboard permission
integrations.manage

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

NameInTypeNotes
Idempotency-Key*headerstring

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

FieldTypeNotes
url*uri
events*WebhookEvent
descriptionstring | null

Responses

  • 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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
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>"
}'

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

GET/webhooks/{id}

Read one webhook endpoint#

Scope
webhooks:read
Rate limit
600/min (read)
Publishable key
refused
Another Site's id
404
Dashboard permission
integrations.manage

Parameters

NameInTypeNotes
id*pathuuid

Responses

  • 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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

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

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

PATCH/webhooks/{id}

Update a webhook endpoint#

Scope
webhooks:write
Rate limit
120/min (write)
Publishable key
refused
Another Site's id
404
Dashboard permission
integrations.manage

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

NameInTypeNotes
id*pathuuid
If-Matchheaderstring

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

FieldTypeNotes
urluri
eventsWebhookEvent
descriptionstring | null
enabledboolean

Responses

  • 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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
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 '{}'

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

DELETE/webhooks/{id}

Delete a webhook endpoint#

Scope
webhooks:write
Rate limit
120/min (write)
Publishable key
refused
Another Site's id
404
Dashboard permission
integrations.manage

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

Parameters

NameInTypeNotes
id*pathuuid

Responses

  • 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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

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

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

POST/webhooks/{id}/rotate-secret

Rotate the signing secret#

Scope
webhooks:write
Rate limit
120/min (write)
Publishable key
refused
Another Site's id
404
Dashboard permission
integrations.manage

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

NameInTypeNotes
id*pathuuid
Idempotency-Key*headerstring

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

  • 200

    The new signing secret. Shown once.

    FieldTypeNotes
    id*uuid
    signing_secret*string

    Shown once. Never retrievable again.

  • 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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
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)"

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

GET/webhooks/{id}/deliveries

List deliveries for an endpoint#

Scope
webhooks:read
Rate limit
600/min (read)
Publishable key
refused
Another Site's id
404
Dashboard permission
integrations.manage

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

NameInTypeNotes
id*pathuuid
cursorquerystring

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.

limitqueryinteger

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.

default 20

statusquerystring

one of pending, delivered, failed

eventqueryWebhookEvent

Return only attempts for this event type.

one of article.created, article.updated, article.deleted, article.published, article.unpublished, article.scheduled, article.unscheduled, category.created, category.updated, category.deleted, tag.created, tag.updated, tag.deleted, author.created, author.updated, author.deleted, media.created, media.updated, media.deleted, pipeline.run.completed, pipeline.run.failed

Responses

  • 200a page of WebhookDelivery

    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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

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

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

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

Send one delivery again#

Scope
webhooks:write
Rate limit
120/min (write)
Publishable key
refused
Plan feature
cms.webhooks
Another Site's id
404
Dashboard permission
integrations.manage

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

NameInTypeNotes
id*pathuuid
delivery_id*pathuuid

Responses

  • 202

    Queued. It will be attempted within the minute.

    FieldTypeNotes
    id*uuid

    The id of the NEW attempt, not of the one you asked to repeat.

    status*"pending"
    attempt*1
  • 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.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 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.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
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"

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

Schemas

The shapes this resource sends and returns. Unknown fields may be added within /v1, so tolerate them.

Webhook#

FieldTypeNotes
id*uuidread only
url*uri
events*WebhookEvent
descriptionstring | null
enabled*boolean

Set to false automatically after repeated delivery failures. Fix your endpoint, then PATCH it back to true. Missed deliveries are not replayed.

disabled_reasonstring | nullread only
last_delivery_atdate-time | nullread only
created_atdate-timeread only

WebhookDelivery#

ONE ATTEMPT. Six attempts of the same event are six of these, all sharing one event_id.

FieldTypeNotes
id*uuidread only

This attempt. Sent as the Writavo-Delivery header, and what you pass to redeliver.

event_id*uuid

Stable across retries of the same event, and equal to the payload's id. Key your idempotency off this, never off the attempt id.

event_type*WebhookEvent

Events you can subscribe to. Every one of them corresponds to a mutation this API can perform, and each fires whether the change came from this API or from the dashboard.

More may be added within v1, so ignore an event type you do not recognise rather than failing the delivery.

article.published is the one most integrations want: it is the signal to rebuild a statically generated site. article.unpublished and article.deleted are the ones people forget, and forgetting them leaves content live on your site after it is gone from ours.

status*string

one of pending, delivered, failed

attempt*integer
exhaustedbooleanread only

True when this was the last attempt and we stopped trying. status: failed with exhausted: false means another attempt is scheduled.

response_statusinteger | null

The HTTP status your endpoint returned. Null if the request never completed.

response_body_excerptstring | nullread only

The first 512 characters of your response, kept so a failure can be diagnosed.

duration_msinteger | nullread only
errorstring | null
created_atdate-timeread only
delivered_atdate-time | null

WebhookEvent#

Events you can subscribe to. Every one of them corresponds to a mutation this API can perform, and each fires whether the change came from this API or from the dashboard.

More may be added within v1, so ignore an event type you do not recognise rather than failing the delivery.

article.published is the one most integrations want: it is the signal to rebuild a statically generated site. article.unpublished and article.deleted are the ones people forget, and forgetting them leaves content live on your site after it is gone from ours.

article.createdarticle.updatedarticle.deletedarticle.publishedarticle.unpublishedarticle.scheduledarticle.unscheduledcategory.createdcategory.updatedcategory.deletedtag.createdtag.updatedtag.deletedauthor.createdauthor.updatedauthor.deletedmedia.createdmedia.updatedmedia.deletedpipeline.run.completedpipeline.run.failed