Reference

Errors & rate limits

One error shape everywhere, and limits at several layers. Knowing which layer rejected you tells you whether to retry, back off, or fix the request.

Error shape #

{ "code": "forbidden", "message": "principal is not a member of this room" }

On the socket, the same error is correlated by your request_id:

{ "v": 1, "type": "error", "request_id": "0b0f2b1e-…",
  "error": { "code": "rate_limited", "message": "too many messages" } }

Statuses #

StatusMeansRetry?
400Malformed or invalid — a field is missing, wrong type, or out of range.No. Fix it.
401Missing, malformed or expired credential.Re-mint, then once.
403Authenticated but not permitted — not a member, or the scope is missing.No.
404Not found — or it exists in another tenant. The two are deliberately indistinguishable.No.
409Conflict — e.g. the room is closed, or a plaintext send into an encrypted room.No.
402Quota exceeded — the tenant's monthly message or byte cap.Not until the cap resets or the plan changes.
413Payload too large.No.
429Rate limited.Yes, with backoff.
408Request timed out server-side (30 s cap).Yes, carefully.
5xxServer fault.Yes, with backoff and jitter.
404 is a security feature. Asking for another tenant's room returns "not found", not "forbidden" — the API will not confirm that an id exists somewhere else.

Rate limits #

LayerDefaultScope
Customer server key120 requests / 10 s (per plan; free 60, pro 600)Per ak_ key
Message send30 / 10 sPer principal — same on REST and socket
Operator control plane1000 / 10 sPer signing key (a backstop, not a product limit)
Account signup / login20 / 30 per minutePer IP
OTP request5 per phone, 30 per IP, 300 per app, per minutePlus a 3-per-minute resend cap
Contacts lookup500 numbers per callPer request
Push per member per room1 / 30 sStorm cooldown, not an error

Rate limiters are fixed-window and fail open if the cache is unavailable — a cache blip degrades enforcement rather than locking everybody out.

Quotas #

Separate from rate limits: plans may carry monthly caps on messages sent and attachment bytes stored. Exceeding one returns 402 quota_exceeded on the metered write only — reads, receipts and control-plane calls keep working, and an idempotent replay of an already-counted operation is never blocked. Check consumption with GET /v1/server/usage.

Retrying safely #

  • Use exponential backoff with jitter. Synchronised retries after an outage are their own outage.
  • Reuse the original client_message_id when retrying a send — that is what makes it idempotent. A fresh id creates a duplicate message.
  • Reuse external_ref when retrying room creation.
  • On 401, mint a fresh token once and retry; do not loop.
  • Never retry 400, 403, 404, 409 or 413 — nothing about the request will change.