Error Handling
How to classify API errors, when to retry, and how to make write retries safe with idempotency keys.
This guide covers how to classify errors from the SurfacedBy Data API, when to retry, and how to retry writes safely.
Every error from the Data API is an RFC 7807 problem document served as application/problem+json. The body carries type, title, status, detail, and request_id; the same request id is echoed in the X-Request-Id response header. See Errors for the document shape and the slug catalogue.
Classify errors first
The fragment of the type URL (for example https://docs.surfacedby.com/v1/api/errors#rate_limited) is the stable switch point. Combine it with the HTTP status:
| Status | Slugs you will see | Retry? |
|---|---|---|
| 400 | idempotency_required | No. Add an Idempotency-Key header to the write and send it again. |
| 401 | missing_api_key, invalid_api_key | No. Fix the X-API-Key header or create a new key. A key is revoked when the workspace leaves Professional or Agency. |
| 402 | domain_limit_reached | No. The workspace tracks as many domains as its plan allows. Remove a domain or upgrade first. |
| 403 | tier_required, scope_required, workspace_access_required, workspace_read_only | No. tier_required: the workspace plan does not include API access (Professional or Agency). scope_required: the key lacks the scope the endpoint needs; write endpoints need the write scope, which only Agency keys created with that scope carry. workspace_access_required: the member who created the key no longer has a role that allows the change. workspace_read_only: changes are paused for the workspace. |
| 404 | domain_not_found, not_found | No. domain_not_found: the domain id does not exist, was removed, or is not accessible to the key. not_found: the competitor, scan, or prompt id does not exist or is not accessible. |
| 409 | domain_already_tracked, pin_limit_reached, prompt_limit_reached | No. The domain is already tracked, the domain has as many pinned prompts as it can hold, or the workspace tracks as many prompts as its plan allows. |
| 409 | idempotency_in_progress | Yes, after the Retry-After delay (5 seconds). The original request with this key is still running. |
| 409 | idempotency_conflict | No. The key was already used with a different body. Resend the original body or use a new key. |
| 422 | validation_error | No. detail says what failed; schema failures also carry an errors array with one entry per field. |
| 429 | rate_limited | Yes, after the Retry-After delay. |
| 500 | internal_error | Yes, with backoff. |
| 503 | service_unavailable, rate_limit_unavailable, service_disabled | Yes, with backoff. Honour Retry-After when present. |
A status without a specific slug falls back to http_<status> (for example #http_404).
Respect Retry-After
Every 429 carries a Retry-After header with the number of seconds to wait, and the problem body's limit_window field says which window you hit (per-minute or per-month). Successful responses carry X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute, X-RateLimit-Limit-Month, and X-RateLimit-Remaining-Month, so you can slow down before you reach a limit.
A per-minute limit resets within the minute. A per-month limit resets at the start of the next UTC calendar month, but its Retry-After is a flat 3600 seconds rather than the time until the reset. When limit_window is per-month, stop and alert instead of retrying. Requests sent while you are over the limit still count against the window. See Rate limits for the per-plan quotas.
Exponential backoff for 5xx
Retry 5xx responses with exponential backoff, not a tight loop: for example 1, 2, 4, 8, 16 seconds, capped at around 30 seconds, with a small random jitter so many clients do not retry in lockstep. A 503 caused by a slow upstream dependency also carries X-Data-Degraded: true.
Stop after a bounded number of attempts (5 is reasonable). Persistent 5xx errors indicate a real problem; retrying forever hides it. Keep the request_id from each failed response so support can trace it.
Idempotency for writes
Write requests (POST and DELETE) require an Idempotency-Key header; without one the API returns 400 idempotency_required. Set the key to a UUID you generate once per logical operation, and reuse the same key and the same body on every retry of that operation.
- If the original request completed, a retry with the same key and body returns the stored response with
X-Idempotent-Replay: trueinstead of running the write again. - If the original is still running, the retry gets 409
idempotency_in_progresswithRetry-After: 5. - If you reuse a key with a different body while the original succeeded or is still running, you get 409
idempotency_conflict. - If the original failed, its key is released, so a corrected retry with the same key is processed as a new request.
See Idempotency for details.
Safe retries checklist
- Switch on the
typeslug and the status. Retry only 429, 5xx, and 409idempotency_in_progress. - Honour
Retry-After, and do not keep retrying against a monthly quota. - Use exponential backoff with jitter for 5xx.
- Cap retry attempts.
- Send an
Idempotency-Keyon every write and keep it stable across retries. - Log the
request_id(body orX-Request-Idheader) so you can correlate retries.