All pages

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

StatusCodeMeaning
401invalid_keyMissing, invalid, or revoked API key.
401account_closedThe account this key belongs to has been closed.
402plan_requiredThe account's current plan does not include the content API.
402payment_requiredA guard behind a write door refused on plan or billing grounds. The message says which.
403key_read_onlyA write door, called with a key created as Read only. Permission is fixed at creation, so this needs a new key.
403wrong_newsroomA 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.
403forbiddenA guard behind a write door refused. The message says which.
404not_foundNo such edition, article or job on your account, a path segment that is not a UUID, or a /v1 path that does not exist.
405method_not_allowedWrong verb for that path. The read endpoints are GET; the four doors that start work are POST.
422invalid_paramsA 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.
429rate_limitedOver a limit. Carries Retry-After: 60.
500errorSomething broke on our side.
501not_implementedA retired endpoint. Only /v1/exports answers this.
503unavailableThe 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.