Errors and limits
Everything both public APIs share: the error shape, what each status means, the rate limits, and how to retry without making things worse.
The error shape
Section titled “The error shape”Every non-2xx response is JSON:
{ "error": "a valid email is required" }The error string is meant for your logs, not your users, and its wording can change. Branch on
the status code.
Status codes
Section titled “Status codes”| Status | What it means | What to do |
|---|---|---|
400 | The request was malformed — bad JSON, a missing required query parameter. | Fix the request. Retrying won’t help. |
401 | The key is missing, unknown, revoked, or the wrong type for this API. | Check the key and which API you’re calling. Don’t retry in a loop — repeated failures throttle your IP. |
403 | You called a server-side API from a browser (an Origin header was present), or a publishable key’s origin allowlist rejected the page. | Move the call to your server, or add the origin to the key. |
404 | No such record — in your factory. | Nothing to retry. |
405 | Wrong HTTP method. The Allow header lists what’s accepted. | Fix the method. |
413 | Body over the 512 KiB cap. | Send less. Split large payloads. |
422 | The JSON parsed but the content is invalid — a malformed email, an unknown enum value. | Fix the data. Retrying identical input fails identically. |
429 | Rate limited. | Wait Retry-After seconds, then retry. |
503 | We couldn’t complete the operation right now. | Retry with backoff. |
A 503 is deliberately not a 401. A database blip on our side must never tell you your key is
bad — that’s a lie you might act on by rotating a perfectly good credential.
Why 401 and not something more specific
Section titled “Why 401 and not something more specific”The public APIs don’t distinguish “no such key”, “revoked key”, “key for the other API”, or “wrong
tenant”. They’re all 401 with the same message. Anything more specific would let someone with a
harvested key learn things about your account by watching how the errors differ.
Same reasoning behind 404 instead of 403 on records that exist but aren’t yours.
Rate limits
Section titled “Rate limits”| Limit | Default | Applies to |
|---|---|---|
| Per publishable key | 6,000/min (100/s) | the bug-reporting API |
| Per secret key | 30,000/min (500/s) | the contacts API |
| Per IP, bug reporting | 1,200/min (20/s) | the bug-reporting API |
| Per IP, contacts | 30,000/min (500/s) | the contacts API |
| Failed keys per IP | 30/min | both, shared — a leaked-key probe gets shut down fast |
Limits are fixed windows. When you exceed one:
HTTP/1.1 429 Too Many RequestsRetry-After: 23
{ "error": "key rate limit" }Retry-After is in seconds: how long until the current window rolls over. Wait that long rather
than guessing — retrying sooner just burns the budget you’re waiting for.
The two APIs have separate per-IP budgets, so a busy website doesn’t eat into your backend’s throughput — and a single server can actually reach the 500/s a secret key allows. The failed-key budget is shared: repeatedly presenting bad keys to either API throttles both from that address.
Need more headroom for a legitimate workload? Ask — the ceilings are configurable, and telling us
about a bulk import beforehand is easier than discovering it as a 429 storm.
Retrying safely
Section titled “Retrying safely”The contacts upsert is idempotent. The contact id is derived from the email, so retrying after
a timeout, a 429, or a 503 updates the same record instead of creating a duplicate. You don’t
need an idempotency key.
Error ingest is idempotent on the eventId your SDK sends, and identical errors are grouped
server-side regardless — a flood becomes one issue, not thousands.
A retry policy that works for both:
- Retry on
429,503, and network/timeout errors. Nothing else. - On
429, sleep exactlyRetry-After. On503, exponential backoff with jitter starting around one second. - Cap at a handful of attempts and log the failure. Don’t retry forever — an infinite retry loop
against a
429is indistinguishable from an attack, and it will be treated like one. - Never retry
4xxother than429. The request is wrong; sending it again won’t make it right.
Body limits
Section titled “Body limits”Both APIs cap request bodies at 512 KiB and answer 413 above it. The bug-reporting API also
caps a batch at 50 events; anything beyond that is explicitly rejected in the per-event
acknowledgements rather than silently dropped, so your ack list always covers everything you sent.
Caching
Section titled “Caching”Responses from the secret API carry Cache-Control: no-store. They’re per-caller data behind a
credential and must not land in a shared cache.