Documentation menu

reference

Articles

The content spine. Create, edit, organise, publish and schedule.

GET/articles

List articles#

Scope
articles:read
Rate limit
600/min (read)
Publishable key
allowed
Dashboard permission
articles.read

Cursor paginated, newest updated first.

The default projection deliberately omits content. Article bodies are large, and a list endpoint that returns every body is the classic way to make a content API slow and expensive. Fetch bodies one at a time with GET /articles/{id}, or ask for them explicitly with fields=id,title,content and a small limit.

There is no way to ask for every field. fields is an allow list, not a wildcard.

A publishable key (wv_pub_) sees only articles at status: published. A secret key sees everything, including drafts.

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

fieldsquerystring

Comma separated field allow list. Any field of the Article schema may be named. Omit for the default projection, which is: id, status, title, slug, excerpt, featured_image_url, category_id, author_id, format_id, published_at, scheduled_publish_at, created_at, updated_at. id is always returned whether or not you name it.

statusqueryArticleStatus[]

Filter by status. Repeat the parameter to match several. A publishable key may only ask for published, and any other value is rejected with 403 INSUFFICIENT_SCOPE.

one of discovered, scored, skipped, scraped, generated, needs_improvement, needs_images, queued, published, rejected, failed, draft, scheduled

category_idqueryuuid
author_idqueryuuid
tag_idqueryuuid

Return only articles carrying this tag.

slugquerystring

Exact slug match. Slugs are unique within a Site, so this returns at most one article.

updated_sincequerydate-time

Return only articles updated at or after this instant. This is the incremental sync parameter: store the greatest updated_at you have seen and pass it back next time.

orderquerystring

one of updated_at.desc, updated_at.asc, published_at.desc, published_at.asc, created_at.desc

default updated_at.desc

Responses

  • 200a page of Article

    A page of articles.

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

  • 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/articles \
  -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/articles

Create an article#

Scope
articles:write
Rate limit
120/min (write)
Publishable key
refused
Dashboard permission
articles.write

Creates an article at status: draft. Always. There is no request field that can make it public, and supplying status is a validation error rather than a silent ignore, so a client written against a different CMS fails loudly instead of quietly leaving content unpublished.

Nothing here is required. An empty body creates an untitled, unslugged draft you can fill in later. title, slug and content do become required at publish time, and POST /articles/{id}/publish returns 422 with a field level breakdown if any is missing.

If you supply a title and no slug, a slug is derived from the title. Supply slug explicitly if the URL matters to you, because a derived slug is not guaranteed stable across versions.

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 (optional)

Shape: ArticleCreate

FieldTypeNotes
titlestring | null
slugstring | null

Derived from title when omitted. Supply it if the URL matters.

contentstring | null

Markdown.

excerptstring | null
featured_image_urluri | null
seo_titlestring | null
seo_descriptionstring | null
seo_keywordsarray | null
faqsarray | null
key_takeawaysarray | null
howto_stepsarray | null
comparisonobject | null
category_iduuid | null
author_iduuid | null
format_iduuid | null
tag_idsuuid[]
An empty draft to fill in later
{}
A complete draft, ready to publish
{
  "title": "How to choose a headless CMS",
  "slug": "how-to-choose-a-headless-cms",
  "content": "## Start with your delivery model\n\nThe first question is not which CMS...",
  "excerpt": "A practical framework for picking a headless CMS without regretting it.",
  "seo_title": "How to choose a headless CMS (2026 guide)",
  "seo_description": "A practical framework for picking a headless CMS.",
  "seo_keywords": [
    "headless cms",
    "content api",
    "jamstack"
  ],
  "category_id": "0f5f1f4e-9c2a-4f7b-9a11-3b5c9d8e7a01",
  "author_id": "6a1c8b22-0d4e-4a9f-8c33-77e2f1a4b5c6",
  "tag_ids": [
    "9d3e2c11-5b6a-4d8e-9f01-2a3b4c5d6e7f"
  ]
}

Responses

  • The draft was created.

    • Location The canonical URL of the new article.
    • ETag The concurrency token. Pass it as If-Match on your first PATCH.
  • 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.

  • 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/articles \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "How to choose a headless CMS",
  "slug": "how-to-choose-a-headless-cms",
  "content": "## Start with your delivery model\n\nThe first question is not which CMS...",
  "excerpt": "A practical framework for picking a headless CMS without regretting it.",
  "seo_title": "How to choose a headless CMS (2026 guide)",
  "seo_description": "A practical framework for picking a headless CMS.",
  "seo_keywords": [
    "headless cms",
    "content api",
    "jamstack"
  ],
  "category_id": "0f5f1f4e-9c2a-4f7b-9a11-3b5c9d8e7a01",
  "author_id": "6a1c8b22-0d4e-4a9f-8c33-77e2f1a4b5c6",
  "tag_ids": [
    "9d3e2c11-5b6a-4d8e-9f01-2a3b4c5d6e7f"
  ]
}'

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/articles/{id}

Read one article#

Scope
articles:read
Rate limit
600/min (read)
Publishable key
allowed
Another Site's id
404
Dashboard permission
articles.read

Returns the full article including content. A publishable key may only read an article at status: published; anything else returns 404, for the same no disclosure reason that governs cross Site access.

Parameters

NameInTypeNotes
id*pathuuid
fieldsquerystring

Comma separated field allow list. Omit to receive every readable field.

Responses

  • The article.

    • ETag The concurrency token for this version of the row.
  • 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/articles/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/articles/{id}

Update an article#

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

A partial update. Only the fields you send are touched. Send null to clear a nullable field; omit it to leave it alone.

status is not updatable here. Use the lifecycle endpoints. Sending status returns 422 VALIDATION_FAILED, which is what stops an API client from pushing an article into the generation engine.

You may edit a published article. The edit goes live on your blog as soon as the CDN cache for that post is purged, which happens as part of this request.

Send `If-Match`. Pass the ETag you received from your last read. If someone else changed the article since then you get 412 PRECONDITION_FAILED instead of silently overwriting their work. If-Match is optional in v1 for compatibility, and omitting it means last write wins, which is almost never what you want on shared content.

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

Shape: ArticleUpdate

FieldTypeNotes
titlestring | null
slugstring | null

Changing the slug of a published article changes its live URL and nothing is redirected for you. The old URL starts returning 404.

contentstring | null

Markdown.

excerptstring | null
featured_image_urluri | null
seo_titlestring | null
seo_descriptionstring | null
seo_keywordsarray | null
faqsarray | null
key_takeawaysarray | null
howto_stepsarray | null
comparisonobject | null
category_iduuid | null
author_iduuid | null
format_iduuid | null
tag_idsuuid[]

Full replacement, not a merge. Send [] to clear.

Responses

  • The updated article.

    • ETag The new concurrency token.
  • 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.

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

  • 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/articles/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/articles/{id}

Delete an article#

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

Permanent. The row and its tag assignments are removed, and if the article was published its URL starts returning 404 on your blog once the cache is purged.

There is no trash and no undo in v1. If you only want to take a post off the web, use POST /articles/{id}/unpublish, which keeps everything and is reversible.

Deleting an article that does not exist returns 404 rather than succeeding, so a double delete is visible to you rather than silent.

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.

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.

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

  • 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/articles/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H 'If-Match: W/"1767225600000"'

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/articles/{id}/publish

Publish an article#

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

Makes the article public immediately, at status: published.

Requires a non empty title, slug and content. If any is missing you get 422 VALIDATION_FAILED with a fields map naming each one, so you can point a user at the exact problem rather than showing a generic failure.

published_at is set to now only if it was not already set. It records when the article was first made public and is the ordering key for your blog, so republishing after an unpublish does not move the post to the top of the feed.

Publishing clears any pending schedule.

This is free. It makes no external call, so it passes no entitlement check, spends no credits and is unaffected by your spend cap. An editor who is not allowed to run the AI pipeline can still publish their own writing.

Safe to repeat: publishing an already published article is a no-op that returns the current state.

Parameters

NameInTypeNotes
id*pathuuid

Responses

  • The article is public.

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

  • 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/articles/3f1b0c7a-0000-4000-8000-000000000001/publish \
  -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/articles/{id}/unpublish

Unpublish an article#

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

Takes the article off the web and returns it to status: draft. The URL starts returning 404 on your blog once the cache is purged.

published_at is deliberately left intact. It is the original publication date and your blog's ordering key, so a post that goes back up keeps its place in the archive.

Nothing is deleted and the operation is fully reversible with POST /articles/{id}/publish.

Parameters

NameInTypeNotes
id*pathuuid

Responses

  • The article is no longer public.

  • 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/articles/3f1b0c7a-0000-4000-8000-000000000001/unpublish \
  -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/articles/{id}/schedule

Schedule an article#

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

Moves the article to status: scheduled and records when it should go live. A cron publishes it within a few minutes of that time, whether or not the AI pipeline is switched on for your Site.

scheduled_publish_at must be in the future. A past or present timestamp is rejected with 422 VALIDATION_FAILED, because silently publishing immediately is the wrong answer to a clock skew bug.

The same title, slug and content requirements as publishing apply, and are checked now rather than at the scheduled moment, so a scheduled post cannot fail silently at two in the morning.

Rescheduling is just another call to this endpoint. Calling it on an already scheduled article replaces the time.

Parameters

NameInTypeNotes
id*pathuuid

Request body

FieldTypeNotes
scheduled_publish_at*date-time

ISO 8601. Include an offset. If you omit one it is read in the Site's timezone, which you can get from GET /site.

Responses

  • The article is scheduled.

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

  • 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/articles/3f1b0c7a-0000-4000-8000-000000000001/schedule \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduled_publish_at": "2026-09-01T09:00:00Z"
}'

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/articles/{id}/cancel-schedule

Cancel a scheduled publish#

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

Returns the article to status: draft and clears scheduled_publish_at. The content is untouched. Calling this on an article that is not scheduled is a no-op that returns the current state.

Parameters

NameInTypeNotes
id*pathuuid

Responses

  • The schedule was cancelled.

  • 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/articles/3f1b0c7a-0000-4000-8000-000000000001/cancel-schedule \
  -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.

Article#

An article. content is markdown, not HTML, in both directions: what you send is what is stored, and it is rendered at publish time. Send markdown.

Several pipeline columns are deliberately absent from this schema rather than exposed as read only, because they describe the engine's internal working rather than your content: the source URL and competitor a draft was researched from (provenance about a third party page, not about your article), the image job handle and image ladder stage (vendor job state that changes shape when we change provider), the raw quality critique (an unstable model output), the source page's SEO score (a number about someone else's page), the dispatcher's priority ordering knob, and the rewrite attempt counter. Exposing any of them would freeze an internal detail into a contract we have promised not to break.

quality_score is exposed, read only, because it is a stable, meaningful number about your own article.

FieldTypeNotes
id*uuidread only
status*ArticleStatusread only

Read only here. Change it with the lifecycle endpoints.

titlestring | null
slugstring | null

The URL segment on your blog. Unique within the Site; a collision is 409 SLUG_CONFLICT. May be null on a draft, and is required to publish.

contentstring | null

The body, in markdown. Not returned in the list default projection; ask for it by name or fetch the article individually.

excerptstring | null
featured_image_urluri | null
seo_titlestring | null
seo_descriptionstring | null
seo_keywordsarray | null
faqsarray | null

Question and answer pairs, rendered as FAQ structured data on your blog.

key_takeawaysarray | null

Short summary bullets rendered above the body.

howto_stepsarray | null

Ordered steps, rendered as HowTo structured data.

comparisonobject | null

A comparison table, rendered as a table in the body.

category_iduuid | null
author_iduuid | null
format_iduuid | null

The content type. See GET /content-types.

tag_idsuuid[]

Every tag on this article. On PATCH this is a full replacement, not a merge: send the complete set you want, and send [] to clear.

quality_scorenumber | nullread only

The engine's quality rating out of 100 for an article it wrote. Null for anything written by hand.

published_atdate-time | nullread only

When the article was first made public. Set by the first publish and never changed after, including across an unpublish and republish, because it is your blog's ordering key.

scheduled_publish_atdate-time | nullread only

Set through POST /articles/{id}/schedule.

created_atdate-timeread only
updated_atdate-timeread only

ArticleCreate#

Every field is optional. The article is created at status: draft regardless of what you send. status, published_at and scheduled_publish_at are not accepted: naming any of them is 422 VALIDATION_FAILED, so a client that assumed it could create published content fails loudly rather than leaving a post silently offline.

FieldTypeNotes
titlestring | null
slugstring | null

Derived from title when omitted. Supply it if the URL matters.

contentstring | null

Markdown.

excerptstring | null
featured_image_urluri | null
seo_titlestring | null
seo_descriptionstring | null
seo_keywordsarray | null
faqsarray | null
key_takeawaysarray | null
howto_stepsarray | null
comparisonobject | null
category_iduuid | null
author_iduuid | null
format_iduuid | null
tag_idsuuid[]

ArticleLifecycleState#

What every lifecycle endpoint returns. Enough to update your UI without a re-read.

FieldTypeNotes
id*uuid
status*ArticleStatus

The article state machine. Thirteen values, in two groups.

Yours, settable through the lifecycle endpoints:

  • draft - private and editable. Where every article starts. Never touched by the AI.
  • scheduled - has a future scheduled_publish_at. A cron publishes it at that time.
  • published - public on your blog.

The pipeline's, readable but not settable. A PATCH naming any of these is rejected with 422 VALIDATION_FAILED:

  • discovered, scored, skipped - a candidate source found and triaged.
  • scraped - source fetched, research signal extracted.
  • generated - a draft the engine wrote, awaiting quality review.
  • needs_improvement - failed quality review; queued for a rewrite.
  • needs_images - text is final, images are being generated.
  • queued - finished and waiting for its publishing slot.
  • rejected, failed - abandoned, or errored past retry.

More pipeline values may be added within v1 as the engine grows. Handle unknown values gracefully.

published_atdate-time | null
scheduled_publish_atdate-time | null
urluri | null

The live URL, when the article is published and the Site has a delivery route configured.

ArticleStatus#

The article state machine. Thirteen values, in two groups.

Yours, settable through the lifecycle endpoints:

  • draft - private and editable. Where every article starts. Never touched by the AI.
  • scheduled - has a future scheduled_publish_at. A cron publishes it at that time.
  • published - public on your blog.

The pipeline's, readable but not settable. A PATCH naming any of these is rejected with 422 VALIDATION_FAILED:

  • discovered, scored, skipped - a candidate source found and triaged.
  • scraped - source fetched, research signal extracted.
  • generated - a draft the engine wrote, awaiting quality review.
  • needs_improvement - failed quality review; queued for a rewrite.
  • needs_images - text is final, images are being generated.
  • queued - finished and waiting for its publishing slot.
  • rejected, failed - abandoned, or errored past retry.

More pipeline values may be added within v1 as the engine grows. Handle unknown values gracefully.

discoveredscoredskippedscrapedgeneratedneeds_improvementneeds_imagesqueuedpublishedrejectedfaileddraftscheduled

ArticleUpdate#

A partial update. Omitted fields are left alone; null clears a nullable field. status, published_at and scheduled_publish_at are rejected with 422 VALIDATION_FAILED.

FieldTypeNotes
titlestring | null
slugstring | null

Changing the slug of a published article changes its live URL and nothing is redirected for you. The old URL starts returning 404.

contentstring | null

Markdown.

excerptstring | null
featured_image_urluri | null
seo_titlestring | null
seo_descriptionstring | null
seo_keywordsarray | null
faqsarray | null
key_takeawaysarray | null
howto_stepsarray | null
comparisonobject | null
category_iduuid | null
author_iduuid | null
format_iduuid | null
tag_idsuuid[]

Full replacement, not a merge. Send [] to clear.