Errors
One error shape, predictable codes.
Every non-2xx response has the same body:
{
"error": {
"code": "insufficient_credits",
"message": "Balance is 3 credits but this request costs 5. Top up at extractr.dev/dashboard/billing."
}
}code— a stable, machine-readable string. Branch on this.message— human-readable detail. May change; don't parse it.
Failed requests are never charged.
Error codes
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | No API key (or session token) supplied. |
| 401 | invalid_api_key | Key malformed, unknown or revoked. |
| 402 | insufficient_credits | Balance below the endpoint's cost. Top up or enable auto top-up. |
| 404 | not_found | Unknown route, or the requested resource doesn't exist. |
| 422 | validation_error | A query/path/body parameter is missing or malformed. |
| 429 | rate_limited | Too many requests — back off and retry. |
| 400 | stripe_not_configured | Billing action attempted while Stripe isn't configured (self-hosted/dev). |
| 500 | internal_error | Something failed on our side. Safe to retry. |
Examples
{
"error": {
"code": "invalid_api_key",
"message": "API key is revoked or does not exist."
}
}{
"error": {
"code": "validation_error",
"message": "Query parameter \"symbol\" is required."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after a short backoff."
}
}Handling errors
Treat 429 and 500 as retryable (with exponential backoff); treat 401, 402 and 422 as bugs or billing state to surface immediately:
const res = await fetch(url, { headers: { "X-API-Key": key } });
if (!res.ok) {
const { error } = await res.json();
switch (error.code) {
case "insufficient_credits":
// alert billing / trigger a top-up
break;
case "rate_limited":
case "internal_error":
// retry with backoff
break;
default:
throw new Error(`${error.code}: ${error.message}`);
}
}