Rate limits and errors
The envelope
Every response from the HTTP API is a JSON object with an ok field. Nothing
is ever returned bare — the board document you asked for arrives under data.
{ "ok": true, "data": { … } }{ "ok": false, "error": "…" }The limit
60 requests a minute, per key. It is a token bucket with continuous refill rather than a fixed window, so a caller cannot spend a whole minute's allowance in the last second of one window and the next minute's in the first second of the following one.
This is a per-process ceiling, not a cluster-wide one. The app may run as more than one machine, and a caller balanced across them can get more than this. Treat 60 a minute as the number to stay under, not as a quota to spend.
A second, quieter ceiling exists for callers presenting keys that do not work:
20 unresolvable keys a minute, per client
address. It is charged only when a key fails to resolve, so an integration
whose key works never meets it. It exists because a loop sending Bearer x
would otherwise cost a database read per request without ever moving the
per-key counter.
Being refused
A throttled request answers 429 and names the wait:
{ "ok": false, "error": "Too many requests — slow down" }The Retry-After header carries a whole number of seconds. Read it. A poller
with no number backs off by doubling, or does not back off at all — which is
why the header is sent rather than left for you to guess.
Every status this API answers with
| Status | What happened | What to do |
|---|---|---|
200 | The read succeeded. The body is the success envelope above. | — |
400 | The cycle you named is not a cycle-shaped string. The message says how it was malformed. | Fix the caller. Retrying will not help. |
401 | The key is missing, malformed, unknown, revoked or expired. One message for all five: Invalid or missing API key. | Check the header, then mint a new key. |
403 | The key is valid; the workspace's plan does not include this. The message comes from the host and names what is missing. | Do not rotate the key — this is a billing answer, not a credential one. |
404 | No such route, no cycle by that shorthand, or a workspace with no cycle to show yet. | Call the cycles endpoint and use an id it returned. |
429 | Over one of the two ceilings above. | Wait the number of seconds in Retry-After. |
500 | Something failed on the server. The message is always Something went wrong reading this board. | Retry with backoff. |
Why the 401 says so little
A single message for missing, malformed, unknown, revoked and expired is a deliberate property, not an oversight: it means probing this API cannot tell a retired key from one that never existed. A caller who revoked their own key knows they did.
The cost is real and it is worth naming, because it lands on you: an integration whose key lapsed overnight is indistinguishable, from outside, from one whose key was typed wrong. The place that can tell you is the key listing inside the product, where an expired row says so in as many words.
Why a 5xx message is always the same sentence
Your logs are a third party's logs. A raw server error here would be a database error string — table names, constraint names, sometimes a fragment of a row — written into a pipeline this workspace does not run and cannot clear. So a 5xx is rewritten before it leaves. Client errors still say what was wrong, because those are yours to fix.
Caching and CORS
Every response carries Cache-Control: private, no-store. Revoking a key is
the only control anybody has over what a leaked one can still read, and an
intermediary that kept serving the board after a revoke would take even that
away. There is no ETag in v1 for the same reason.
There is no Access-Control-Allow-Origin header, at all. That absence is
the statement: an API key is a secret held by one integration, and a permissive
CORS header would let any page in any browser read a workspace's board out of a
session that happened to carry the key. A caller for this API is a server, and
a server does not ask about CORS.