Errors & retries
Every failure — a bad parameter, an auth problem, a capacity event — uses exactly one JSON envelope. If your error handling covers this shape, it covers the entire API.
The error envelope
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: req_9f12…
X-SharpRelay-Origin: https://api.worksinprod.dev
{
"error": {
"code": "invalid_parameters",
"message": "One or more request parameters are invalid.",
"request_id": "req_9f12…"
}
}
| Field | Meaning |
|---|---|
code | Stable, machine-readable identifier — branch on this, never on message |
message | Human-readable explanation; may change between releases |
request_id | Unique ID of this request (also in the X-Request-Id header) — quote it for support |
Data-plane error codes
These are the stable failures returned by the 12 data endpoints. Account, checkout, and credential-delivery operations have additional route-specific 4xx codes in the API reference; they use the same envelope.
| Status | Code | Meaning | Retryable? |
|---|---|---|---|
| 400 | invalid_parameters | A query parameter is missing or malformed | No — fix the request |
| 401 | invalid_api_key | Missing, malformed, or revoked key | No |
| 403 | insufficient_scope | The key does not have data:read | No — use a correctly scoped key |
| 403 | account_suspended / plan_inactive | The account or plan cannot access data | No — resolve the account state |
| 404 | not_found | The requested resource or result does not exist | No |
| 429 | rate_limited | Per-second rate limit hit | Yes — after Retry-After (1s) |
| 429 | quota_exceeded | UTC-daily guard or plan-period allowance spent | Yes — after Retry-After (first applicable reset) |
| 500 | internal_error | Unexpected gateway failure | Yes — with backoff; report if persistent |
| 502 | service_error | SharpRelay could not safely complete the data response | Yes — with backoff |
| 503 | temporarily_unavailable | Accounting, capacity, or dependency unavailable | Yes — honor Retry-After when present, otherwise back off |
Errors and credit charges
Authentication, local parameter validation, and failures before a data
request is dispatched are not charged. If work reached the contracted data
source, a normalized 400, 404, 502, or 503 response can cost one credit.
Metering headers are present only when a response was metered:
X-Credits-Cost is authoritative, and
X-Result-Rows is zero for a metered error. Their absence means no
buyer credit was charged.
Historical Request Health
The authenticated buyer dashboard keeps request diagnosis beside usage and quota information. Request Health shows daily success rate, dominant normalized error codes, recent failed request IDs, owned endpoint names, latency, and the exact credit charge for each recorded failure. Automatic guidance explains the safest correction, while an active support note can add account-specific advice.
GET /v1/account/request-health?days=30 exposes the same
tenant-isolated data for automation. Detailed failures are retained for 30
days; daily and issue aggregates support longer trends. The history never
stores or returns query values, bodies, authorization headers, IP addresses,
or private-source details. Authentication failures cannot be assigned safely
to an account and are therefore not included. Use the request ID from the
live error response when contacting support about one of those failures.
Recommended retry strategy
Retry only the retryable codes above, with exponential backoff and jitter, capped at a handful of attempts:
def call_with_retry(req, max_attempts=5):
for attempt in range(max_attempts):
try:
resp = req()
except NetworkError:
resp = None
if resp is not None and resp.status < 500 and resp.status != 429:
return resp # success or a non-retryable error
if attempt == max_attempts - 1:
raise
wait = min(2 ** attempt, 30) * (0.5 + random())
retry_after = resp.headers.get("Retry-After") if resp else None
sleep(float(retry_after) if retry_after else wait)
Honor Retry-After whenever present — it reflects the
actual safe retry time, not a guess. Add jitter so parallel workers don't
retry in lockstep. For data-plane calls, never retry 4xx other than 429 — they fail
deterministically.
Credential-delivery retries are exact replays
Do not apply a generic “create again” retry to credential mutations. If a
key-rotation response is interrupted, retry with the exact old API key and the
exact original rot_… Idempotency-Key during its
15-minute window. If a checkout claim or lost-key recovery response is
interrupted, retry with the exact same fragment bearer during its 10-minute
delivery window. Changing either secret starts no safe recovery and can leave
you without the only copy. Treat a received replacement as sensitive even when
your client later times out.
Client-side safety net
Build a local circuit breaker for your own protection: if you see
repeated quota_exceeded or rate_limited responses,
slow your polling loop down or drop to a cheaper endpoint class — see
Quotas & metering and
Polling & performance.