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 codes
StatusWhat happenedWhat to do
200The read succeeded. The body is the success envelope above.—
400The cycle you named is not a cycle-shaped string. The message says how it was malformed.Fix the caller. Retrying will not help.
401The 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.
403The 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.
404No 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.
429Over one of the two ceilings above.Wait the number of seconds in Retry-After.
500Something 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.