Rate limits & errors
What to expect when you hit a limit, and how every error is shaped so your client can handle it without guessing.
Limits
Reads are capped at 120 requests per minute per key, counted across all of /v1 on a rolling window. Go over and you get a 429 with a Retry-After: 60 header. Design for that number; back off when you see it rather than retrying immediately.
Failed authentications are capped separately at 30 per minute per address, which also answers 429. That budget is spent by rejected keys, not by successful calls, and it is shared by everyone calling from the same address.
A key created with Full access carries a third limit: 20 paid actions an hour, per key. An article, one row of a batch, a research report and a domain audit each count one. Over it is a 429 with the same Retry-After, and nothing is started - except a batch, which is cut at the room left and reports every row it could not start. See Full access & MCP.
Reads never consume your article allowance. That meters articles your newsroom writes, and a pull costs nothing to run - which is why pulling the same article twice is not something you need to engineer around. Starting work does consume it, out of the same allowance the dashboard spends from.
The cheapest way to stay well under the limit is not to re-fetch what you already have: keep the newest created_at you have stored and pass it as ?since= on the next run. See List articles.
The error envelope
Every error response is JSON, shaped the same way regardless of status code. That holds for framework-level failures too - a mistyped path or a wrong verb comes back in this shape, not as an HTML error page.
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded (120 requests/minute)"
}
}Branch on error.code, which is stable, rather than on the message, which is written for a human reading a log.
Status codes
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_key | Missing, invalid, or revoked API key. |
| 401 | account_closed | The account this key belongs to has been closed. |
| 402 | plan_required | The account's current plan does not include the content API. |
| 402 | payment_required | A guard behind a write door refused on plan or billing grounds. The message says which. |
| 403 | key_read_only | A write door, called with a key created as Read only. Permission is fixed at creation, so this needs a new key. |
| 403 | wrong_newsroom | A full-access key spends only in the newsroom its owner was in when it was created; if that person now acts in a different newsroom, create a new key there. A read-only key is not gated this way. |
| 403 | forbidden | A guard behind a write door refused. The message says which. |
| 404 | not_found | No such edition, article or job on your account, a path segment that is not a UUID, or a /v1 path that does not exist. |
| 405 | method_not_allowed | Wrong verb for that path. The read endpoints are GET; the four doors that start work are POST. |
| 422 | invalid_params | A parameter or body field did not parse: status other than draft or published, a since that is not ISO-8601, a missing input, or two batch rows sharing an id. |
| 429 | rate_limited | Over a limit. Carries Retry-After: 60. |
| 500 | error | Something broke on our side. |
| 501 | not_implemented | A retired endpoint. Only /v1/exports answers this. |
| 503 | unavailable | The API is misconfigured, or a plan, ceiling or data store could not be read. It refuses rather than guessing. Retry. |
Asking for something that belongs to another account is a 404, the same answer as something that does not exist, because a 403 would confirm it does. The 403s above are never about whose row it is: they are about what this key may do, which is a question that can be answered without confirming anything.
Branching on the code is worth doing here. A 402 or a 403 will not clear on a retry - the key, the plan or the permission has to change - while a 429 or a 503 will.
Retired endpoints
The bulk export endpoints - /v1/exports and its job and download paths - are not part of this API. They answer 501 rather than 404, so a client written against the older contract gets a parseable answer instead of something that reads like a missing edition.
{
"error": {
"code": "not_implemented",
"message": "Export is not available on this API. Export your content from the dashboard."
}
}A single archive of everything you own is a dashboard job: Settings → Account → Download everything. The read endpoints already serve your editions, your articles and their images to your own code, which is what an integration actually needs.