Documentation menu

errors

Every code, and what to do about it

Branch on error.code. It is stable within /v1; the message is not, and may be reworded at any time.

The shape of a failure#

Every failed response has this shape.

422
{
  "ok": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Add a title before publishing.",
    "fields": {
      "title": "Add a title before publishing."
    },
    "request_id": "0f2a8c31-0000-4000-8000-00000000abcd"
  }
}
  • code is what you switch on. Treat an unknown code as a generic failure of its HTTP status class rather than crashing: new codes may be added within /v1.
  • fields is present on VALIDATION_FAILED and maps each offending field to a message fit to put next to an input.
  • request_id identifies the exact request in our logs. Quote it when you contact support.
  • A message never contains a stack trace, SQL, an internal identifier, or anything belonging to another customer.
Every code below has its own anchor, so an error handler can link a person straight to the paragraph that explains what they hit: https://writavo.com/docs/errors#NOT_ENTITLED.

The three different 402s#

These are the ones worth reading before you hit them, because they look identical in a log and have three different fixes. They are 402 rather than 403 deliberately: a 403 says you may not, and each of these says not yet, and here is how to change that.

They are checked in that order, so a caller who is not on the right plan never learns anything about the balance.

Check before you spend

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

Read this before a pipeline run if you would rather fail fast than handle a 402.

The full catalog#

Malformed JSON, a bad parameter, or a missing required header.

What to do. Fix the request before retrying. Common causes: a body that is not JSON, an unknown value in a query parameter, a cursor you constructed rather than passed back, or a missing Idempotency-Key on a creating POST.

No key, an unparseable key, or one that does not exist.

What to do. Check the key is present, complete, and sent as Authorization: Bearer <key>. This response never distinguishes a missing key from a wrong one, so you cannot tell from it which mistake you made.

The key was revoked or rotated past its grace window.

What to do. Someone revoked or rotated this key. Create a new one in the dashboard and update your integration. Nothing you can send will make this key work again.

The key passed its expires_at.

What to do. The key passed its expires_at. Create a replacement. If you did not expect an expiry, check whether the key was created with one.

The key is valid but lacks the scope this operation needs. The only 403 in the API.

What to do. Either the key lacks the scope, or its creator's permissions no longer cover it. Check GET /keys for the scopes actually granted, then check the creator still has the matching dashboard permission. This is the only 403 in the API, and it never means the object belongs to someone else.

Your plan does not include this AI pipeline capability. Upgrade. CMS capabilities are on every plan and never return this.

What to do. Your plan does not include this capability. Upgrading is the only fix; topping up credits will not help. This is answered before the credit check, so it tells you nothing about your balance.

The organisation cannot afford the next unit of work. Top up.

What to do. The organisation cannot afford the next unit of work. Top up. The balance is shared by every Site under the account, so another Site may have spent it.

This Site hit its own monthly ceiling. Raise it or wait.

What to do. This Site hit the monthly ceiling you configured for it, not a platform limit. Raise the cap in the dashboard or wait for the period to reset. GET /usage shows the cap, what has been spent against it, and whether it is reached.

This would go past your included CMS allowance and there is no payment method on file. Not a plan limit and not an upgrade: CMS resources are pay-as-you-go on every plan. Add a card and retry. Nothing already published stops serving.

What to do. You have used everything included with your account for this resource, and there is no payment method on file to bill the rest to. Add a card and retry; the same request will then succeed. This is not a plan limit and upgrading will not fix it: storage, documents, seats, Sites and domains are pay-as-you-go on every plan including Free, so the fix is a card rather than a tier. Nothing you have already published stops serving while this is outstanding, and GET /usage shows which resource ran out and what the next unit costs.

No such object, or it belongs to another Site. Deliberately indistinguishable.

What to do. Either there is no such object, or it belongs to a different Site. If you are certain the id is right, check you are using the key for the correct Site.

Another object on this Site already uses that slug.

What to do. Another object on this Site already uses that slug. Pick a different one, or let the API derive one by omitting slug.

The key was reused with a different request body.

What to do. You reused a key with a different body. That is a bug in the client rather than a retry: generate one key per logical operation and reuse it only to repeat that same operation.

The first request with this key is still running. Retry shortly.

What to do. The first request with this key is still running. Wait a moment and retry with the same key; you will get the original response replayed.

The object changed under you mid request. Re-read and retry.

What to do. The object changed under you mid request. Re-read it and retry.

Your If-Match did not match. Someone else edited it. Re-read and retry.

What to do. Your If-Match did not match, so someone edited the object since you read it. Nothing was written. Re-read, merge your change on top, and retry with the new ETag.

The request parsed but the values are not acceptable. See fields.

What to do. Read error.fields. It maps each offending field to a message you can show next to the input. The three most common causes are publishing without a title, slug or content; scheduling in the past; and sending status, which is read only.

Too many requests. Back off and honour Retry-After.

What to do. Back off and honour Retry-After. Watch RateLimit-Remaining on successful responses so you can slow down before you are refused rather than after.

Writes are paused for maintenance. Retry later.

What to do. Writes are paused. Reads usually keep working and your published blog is served from cache, so your site stays up. Retry after the window in Retry-After.

Our fault. Safe to retry an idempotent request.

What to do. Ours, not yours. Safe to retry, and safer with the same Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. Quote the request_id if you contact support.

Why a wrong Site returns 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.

An id that is not yours

returns 404
curl
curl https://api.writavo.com/v1/articles/00000000-0000-4000-8000-000000000000 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"

An object belonging to another Site answers exactly like an object that does not exist. That is what stops anyone enumerating other customers' content by id.