Operations
Running in production.
How errors come back, how paging works, how to send a message exactly once, and the rate limits in force. Plus the CORS setting that gates in-browser calls.
Errors
Errors are JSON with a human-readable error and, for validation failures, a details map of field to reasons:
{
"error": "Validation failed",
"details": {
"query": ["query is required"],
"limit": ["Number must be less than or equal to 50"]
}
} | Status | Means |
|---|---|
400 | Request failed schema validation. Check details. |
401 | Missing, malformed, revoked, or expired key. Also if the key owner is no longer active. |
403 | Authenticated, but not allowed to touch this resource. |
404 | Resource not found, or your key lacks the scope to see it exists. |
204 | Success with no body (e.g. a delete). |
429 | Rate limited. See below. |
Pagination
List endpoints are cursor-paginated. Pass limit for page size and
after to continue. Each response carries hasMore and
an opaque cursor; feed that cursor back as after until
hasMore is false.
curl "/api/v1/workspaces//streams?limit=50" \
-H "Authorization: Bearer " {
"data": [ /* … */ ],
"hasMore": true,
"cursor": "eyJ2IjoxLCJzIjoiMjAyNi0wNS0yOC…"
} sequence via
before/after (numeric), with a hasMore
flag and no opaque cursor. And when you pass a free-text query to a
list endpoint, results rank by relevance and cursoring is turned off.
Idempotency
Sending a message accepts an optional clientMessageId (up to 128
characters). Retrying with the same stream and clientMessageId
won't create a duplicate. You get the original message back, with the same
201. Generate one ID per logical message and reuse it across
retries.
curl -X POST /api/v1/workspaces//streams/STREAM_ID/messages \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"content": "Deploy finished: v2.4.1 is live.",
"clientMessageId": "deploy-2.4.1-notify"
}'
Replace STREAM_ID with a real stream (list streams above to find
one). The dedupe is a database constraint, so it holds even for concurrent
retries.
Rate limits
Several limits stack, each over a rolling 60-second window. A request has to clear all of them, so the tightest one that applies is what you actually feel. These are the current defaults and may be tuned.
| Limit | Window | Scope | Applies to |
|---|---|---|---|
| 60 requests | 60s | per API key | every endpoint |
| 600 requests | 60s | per workspace | every endpoint |
| 300 requests | 60s | per client IP | baseline, all routes |
| 20 requests | 60s | per caller | attachment uploads |
For a single key on a single host, the 60/min per key limit is the one you'll hit first. The per-IP baseline matters when many keys share one machine; the per-workspace ceiling caps everyone together. Uploads are held tighter still.
Whichever you hit first returns 429. The body reports the limit and
window, and every response carries the standard RateLimit-* headers
so you can back off before you're cut off:
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 23
{ "error": "Rate limit exceeded", "limit": 60, "windowMs": 60000 }
Watch RateLimit-Remaining and pause until
RateLimit-Reset seconds have passed. To go faster, spread work
across keys and hosts rather than pushing one key past 60/min.
CORS
The allow-list is the CORS_ALLOWED_ORIGINS environment variable on
the backend (comma-separated origins). Add the docs origin there and the
in-browser Run button works. For your own integrations this never applies: a
server or CLI client has no preflight to satisfy.