reference
Articles
The content spine. Create, edit, organise, publish and schedule.
/articlesList 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
| Name | In | Type | Notes |
|---|---|---|---|
| cursor | query | string | The opaque cursor from |
| limit | query | integer | Page size. Values above the maximum are clamped rather than rejected, so a client asking for a thousand rows gets a hundred and a default 20 |
| fields | query | string | Comma separated field allow list. Any field of the Article schema may be named. Omit for the default projection, which is: |
| status | query | ArticleStatus[] | Filter by status. Repeat the parameter to match several. A publishable key may only ask for one of discovered, scored, skipped, scraped, generated, needs_improvement, needs_images, queued, published, rejected, failed, draft, scheduled |
| category_id | query | uuid | |
| author_id | query | uuid | |
| tag_id | query | uuid | Return only articles carrying this tag. |
| slug | query | string | Exact slug match. Slugs are unique within a Site, so this returns at most one article. |
| updated_since | query | date-time | Return only articles updated at or after this instant. This is the incremental sync parameter: store the greatest |
| order | query | string | 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_REVOKEDorAPI_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 honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/articlesCreate 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
| Name | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key* | header | string | 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 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
| Field | Type | Notes |
|---|---|---|
| title | string | null | |
| slug | string | null | Derived from |
| content | string | null | Markdown. |
| excerpt | string | null | |
| featured_image_url | uri | null | |
| seo_title | string | null | |
| seo_description | string | null | |
| seo_keywords | array | null | |
| faqs | array | null | |
| key_takeaways | array | null | |
| howto_steps | array | null | |
| comparison | object | null | |
| category_id | uuid | null | |
| author_id | uuid | null | |
| format_id | uuid | null | |
| tag_ids | uuid[] |
{}{
"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
- 201Article
The draft was created.
LocationThe canonical URL of the new article.ETagThe concurrency token. Pass it asIf-Matchon 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_REVOKEDorAPI_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_FLIGHTorCONFLICT. 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. Readcodeto tell which, then re-read and retry. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps 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
statuson 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 honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid | |
| fields | query | string | Comma separated field allow list. Omit to receive every readable field. |
Responses
- 200Article
The article.
ETagThe concurrency token for this version of the row.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_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 honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid | |
| If-Match | header | string | The Optional in v1 for compatibility. Omitting it means last write wins. Send it. |
Request body
Shape: ArticleUpdate
| Field | Type | Notes |
|---|---|---|
| title | string | null | |
| slug | string | null | Changing the slug of a published article changes its live URL and nothing is redirected for you. The old URL starts returning 404. |
| content | string | null | Markdown. |
| excerpt | string | null | |
| featured_image_url | uri | null | |
| seo_title | string | null | |
| seo_description | string | null | |
| seo_keywords | array | null | |
| faqs | array | null | |
| key_takeaways | array | null | |
| howto_steps | array | null | |
| comparison | object | null | |
| category_id | uuid | null | |
| author_id | uuid | null | |
| format_id | uuid | null | |
| tag_ids | uuid[] | Full replacement, not a merge. Send |
Responses
- 200Article
The updated article.
ETagThe 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_REVOKEDorAPI_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_FLIGHTorCONFLICT. 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. Readcodeto tell which, then re-read and retry. - 412
PRECONDITION_FAILED. YourIf-Matchdid not match the current version, meaning somebody edited the object since you read it. Nothing was written. Re-read, merge, and retry with the newETag. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps 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
statuson 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 honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid | |
| If-Match | header | string | The Optional in v1 for compatibility. Omitting it means last write wins. Send it. |
Responses
- 204
Deleted. No body.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_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. YourIf-Matchdid not match the current version, meaning somebody edited the object since you read it. Nothing was written. Re-read, merge, and retry with the newETag. - 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/articles/{id}/publishPublish 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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid |
Responses
The article is public.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_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_FLIGHTorCONFLICT. 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. Readcodeto tell which, then re-read and retry. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps 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
statuson 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 honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/articles/{id}/unpublishUnpublish 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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid |
Responses
The article is no longer public.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_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_FLIGHTorCONFLICT. 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. Readcodeto tell which, then re-read and retry. - 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/articles/{id}/scheduleSchedule 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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid |
Request body
| Field | Type | Notes |
|---|---|---|
| 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 |
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_REVOKEDorAPI_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_FLIGHTorCONFLICT. 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. Readcodeto tell which, then re-read and retry. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps 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
statuson 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 honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
/articles/{id}/cancel-scheduleCancel 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
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid |
Responses
The schedule was cancelled.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_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_FLIGHTorCONFLICT. 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. Readcodeto tell which, then re-read and retry. - 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-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-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin 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 inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
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.
| Field | Type | Notes |
|---|---|---|
| id* | uuidread only | |
| status* | ArticleStatusread only | Read only here. Change it with the lifecycle endpoints. |
| title | string | null | |
| slug | string | null | The URL segment on your blog. Unique within the Site; a collision is |
| content | string | null | The body, in markdown. Not returned in the list default projection; ask for it by name or fetch the article individually. |
| excerpt | string | null | |
| featured_image_url | uri | null | |
| seo_title | string | null | |
| seo_description | string | null | |
| seo_keywords | array | null | |
| faqs | array | null | Question and answer pairs, rendered as FAQ structured data on your blog. |
| key_takeaways | array | null | Short summary bullets rendered above the body. |
| howto_steps | array | null | Ordered steps, rendered as HowTo structured data. |
| comparison | object | null | A comparison table, rendered as a table in the body. |
| category_id | uuid | null | |
| author_id | uuid | null | |
| format_id | uuid | null | The content type. See |
| tag_ids | uuid[] | Every tag on this article. On |
| quality_score | number | nullread only | The engine's quality rating out of 100 for an article it wrote. Null for anything written by hand. |
| published_at | date-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_at | date-time | nullread only | Set through |
| created_at | date-timeread only | |
| updated_at | date-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.
| Field | Type | Notes |
|---|---|---|
| title | string | null | |
| slug | string | null | Derived from |
| content | string | null | Markdown. |
| excerpt | string | null | |
| featured_image_url | uri | null | |
| seo_title | string | null | |
| seo_description | string | null | |
| seo_keywords | array | null | |
| faqs | array | null | |
| key_takeaways | array | null | |
| howto_steps | array | null | |
| comparison | object | null | |
| category_id | uuid | null | |
| author_id | uuid | null | |
| format_id | uuid | null | |
| tag_ids | uuid[] |
ArticleLifecycleState#
What every lifecycle endpoint returns. Enough to update your UI without a re-read.
| Field | Type | Notes |
|---|---|---|
| id* | uuid | |
| status* | ArticleStatus | The article state machine. Thirteen values, in two groups. Yours, settable through the lifecycle endpoints:
The pipeline's, readable but not settable. A
More pipeline values may be added within v1 as the engine grows. Handle unknown values gracefully. |
| published_at | date-time | null | |
| scheduled_publish_at | date-time | null | |
| url | uri | 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 futurescheduled_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_imagesqueuedpublishedrejectedfaileddraftscheduledArticleUpdate#
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.
| Field | Type | Notes |
|---|---|---|
| title | string | null | |
| slug | string | null | Changing the slug of a published article changes its live URL and nothing is redirected for you. The old URL starts returning 404. |
| content | string | null | Markdown. |
| excerpt | string | null | |
| featured_image_url | uri | null | |
| seo_title | string | null | |
| seo_description | string | null | |
| seo_keywords | array | null | |
| faqs | array | null | |
| key_takeaways | array | null | |
| howto_steps | array | null | |
| comparison | object | null | |
| category_id | uuid | null | |
| author_id | uuid | null | |
| format_id | uuid | null | |
| tag_ids | uuid[] | Full replacement, not a merge. Send |