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.
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.
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. |
| 404 | not_found | No such edition or article 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. Every /v1 endpoint is a GET. |
| 422 | invalid_params | A parameter did not parse: status other than draft or published, or a since that is not ISO-8601. |
| 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 the data store could not be reached. Retry. |
There is no 403. Asking for something that belongs to another account returns 404, the same answer as something that does not exist, because a 403 would confirm it does.
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 four endpoints above already serve your editions, your articles and their images to your own code, which is what an integration actually needs.