Error Codes
All errors follow the envelope format with a SCREAMING_SNAKE_CASE code field.
Retry guidance
Every error also says, in machine-readable form, whether to retry, when, and what to change instead. The fields sit next to code and message on every error from API v1, API v2, check-windows, the CVE endpoints and authentication:
| Field | Type | Meaning |
|---|---|---|
retryable | boolean | true only when repeating the exact same request later may succeed. Never retry when false. |
retry_after_seconds | integer or null | Seconds to wait before retrying, when the server knows (rate limits, server errors). The same value is sent as the standard Retry-After header. |
action | string | What to do: retry_later, wait_until_reset, reduce_batch, fix_input, use_different_key or contact_us |
reset_at | string | Only on usage limits that reset: when they do (ISO 8601, UTC). Also in detail as resets_at (partner keys) or reset_at |
max | integer | Only on batch limits: the per-call maximum. Also in detail.max |
The existing code, message and detail are unchanged.
{
"api": { "version": "1.0.0", "request_id": "bs-req-...", "timestamp": "2026-10-01T15:00:00Z" },
"error": {
"code": "BATCH_TOO_LARGE",
"message": "At most 25 hosts per call. Split the hosts into batches of 25 or fewer and send one call per batch.",
"retryable": false,
"retry_after_seconds": null,
"action": "reduce_batch",
"max": 25,
"detail": { "code": "BATCH_TOO_LARGE", "error": "batch_too_large", "message": "...", "max": 25, "received": 30 }
}
}
Classification
| HTTP | error.code (detail.error) | retryable | retry_after_seconds | action |
|---|---|---|---|---|
| 429 | RATE_LIMITED (rate_limited): per-key limit, v1 and v2 | true | seconds until the limit window ends | retry_later |
| 429 | RATE_LIMITED: edge limit for /api/ (nginx) | true | 10 | retry_later |
| 429 | PARTNER_LIMIT: a partner key's monthly asset checks are used up | false | null | wait_until_reset (reset_at) |
| 429 | TRIAL_ENDED (detail.reason = limit): the trial's asset checks are used up; they do not reset | false | null | contact_us |
| 429 | RATE_LIMITED (query_limit): AI assistant monthly or daily limit | false | null | wait_until_reset (reset_at) |
| 429 | RATE_LIMITED (free_query_limit): AI assistant without an account | false | null | use_different_key |
| 403 | TRIAL_ENDED (expired or ended) | false | null | contact_us |
| 409 | TRIAL_ENDED, TRIAL_UNAVAILABLE (starting a trial) | false | null | contact_us |
| 413 | BATCH_TOO_LARGE (batch_too_large), TRIAL_BATCH_LIMIT | false | null | reduce_batch (max) |
| 400 | UNKNOWN_PARAMETER, other bad input | false | null | fix_input |
| 422 | VALIDATION_ERROR; refused hosts in data.rejected (identifying fields and other per-host codes) | false | null | fix_input |
| 404, 405, 410 | NOT_FOUND, METHOD_NOT_ALLOWED, gone | false | null | fix_input |
| 409 | conflicts such as "already watching" | false | null | fix_input |
| 403 | FORBIDDEN (confirmation_required) | false | null | fix_input |
| 401 | AUTH_REQUIRED: missing, invalid, expired or revoked key, or an expired session | false | null | use_different_key |
| 403 | PARTNER_SCOPE, TRIAL_SCOPE: the key cannot call this endpoint | false | null | contact_us |
| 403 | TRIAL_REQUIRED, API_ONLY_ACCOUNT, API_KEY_REQUIRED, DEMO_EXAMPLES_ONLY | false | null | use_different_key |
| 403 | FORBIDDEN (insufficient_scope, viewer_role, demo_read_only including permission: demo_read_only, session_required, other) | false | null | use_different_key |
| 402, 403 | TIER_GATE, FORBIDDEN (upgrade_required, tier_required, cap_exceeded, limit_reached) | false | null | contact_us |
| 500 | INTERNAL_ERROR, including database timeouts | true | 5 | retry_later |
| 502 | upstream failure (ERROR from the API, SERVICE_UNAVAILABLE from the edge) | true | 10 | retry_later |
| 503 | ERROR (database unavailable), SERVICE_UNAVAILABLE from the edge | true | 30 | retry_later |
| 503 | ERROR (demo_unavailable): the public demo account is switched off | false | null | contact_us |
| 504 | GATEWAY_TIMEOUT from the edge | true | 30 | retry_later |
Each host refused in data.rejected (API v2 and check-windows) carries retryable: false, retry_after_seconds: null and action: fix_input.
Cloudflare's own pages (for example its 524 timeout or its own 429) are outside BreachSpider and carry no guidance. Treat them by status: 429 and 5xx are retryable, everything else is not. The official Python SDK does this automatically.
| Code | HTTP Status | Meaning |
|---|---|---|
| UNKNOWN_PARAMETER | 400 | A query parameter the endpoint does not accept; detail lists the accepted parameters |
| AUTH_REQUIRED | 401 | Valid session cookie or API key required. The message says Invalid API key, API key expired or API key revoked |
| FORBIDDEN | 403 | Authenticated but not authorized, including tier upgrades required (detail carries error: upgrade_required) |
| viewer_role | 403 | Read-only (viewer) member attempted a write operation |
| TIER_GATE | 402 | Returned by a small number of endpoints (audit log export, AI assistant chat). Most tier gating returns 403 FORBIDDEN |
| NOT_FOUND | 404 | Resource does not exist |
| METHOD_NOT_ALLOWED | 405 | HTTP method not supported on this endpoint |
| BATCH_TOO_LARGE | 413 | Batch too large: more than 200 assets in one correlate-cves call, or more than 25 hosts in one Windows call (detail.max, detail.received; detail.error = batch_too_large) |
| VALIDATION_ERROR | 422 | Request parameters failed validation, e.g. sort not one of priority, score, exploit, newest |
| RATE_LIMITED | 429 | Too many requests: wait retry_after_seconds (or the Retry-After header) and retry |
| PARTNER_SCOPE | 403 | A partner key called an endpoint outside its scope. Read-only partner keys can call correlate-cves, correlate-cves/check, check-windows and the CVE endpoints |
| PARTNER_LIMIT | 429 | A partner key's monthly asset-check limit would be passed (detail.used, limit, remaining, requested, resets_at). Nothing was processed |
| INTERNAL_ERROR | 500 | Server error - our team has been notified |
Free API Trial Errors
Returned only for keys on the free API trial. Each carries detail.contact_url, the scoping email.
| Code | HTTP Status | Meaning |
|---|---|---|
| TRIAL_ENDED | 403 | The trial is over: detail.reason is expired (14 days passed) or ended (ended early). The key is genuine but no longer accepted |
| TRIAL_ENDED | 429 | The trial's 750 asset checks are used up, or this request needs more than remain (detail.reason = limit, with used, limit, remaining and requested). Nothing in the request was processed |
| TRIAL_SCOPE | 403 | Trial keys can call POST /assets/correlate-cves, POST /assets/correlate-cves/check and GET /cves/{cve_id} only |
| TRIAL_BATCH_LIMIT | 413 | More than 25 assets in one request (detail.max, detail.received) |
| TRIAL_REQUIRED | 403 | A free account called correlate-cves or its check. Checking your own devices needs a free trial (detail.trial_url) |
API v2 (Windows Patch Level) Errors
API v2 uses the same envelope. error.detail.error carries the specific code: insufficient_scope (403), environment_not_found (404), batch_too_large (413), rate_limited (429, with retry_after). Hosts refused by validation are listed per host in data.rejected with codes such as forbidden_field, placeholder_value and missing_field. See Windows Patch Level.
Tier Upgrade Response
When a feature is gated by tier, most endpoints return HTTP 403 with code FORBIDDEN and an upgrade_required detail block. For example, generating an API key on Free or Standard returns:
{
"api": { "version": "1.0.0", "request_id": "bs-req-..." },
"error": {
"code": "FORBIDDEN",
"message": "API access is available on the Professional tier and above.",
"retryable": false,
"retry_after_seconds": null,
"action": "contact_us",
"detail": {
"error": "upgrade_required",
"permission": "api_access",
"current_tier": "standard",
"upgrade_url": "/account/upgrade",
"message": "API access is available on the Professional tier and above."
}
}
}
A small number of endpoints (audit log export, AI assistant chat) instead return HTTP 402 with code TIER_GATE. Handle both 402 and 403 as "upgrade required."
VALIDATION_ERROR Response
Validation errors include per-field detail:
{
"api": { "version": "1.0.0", "request_id": "bs-req-..." },
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"retryable": false,
"retry_after_seconds": null,
"action": "fix_input",
"detail": [
{
"field": "query.severity",
"message": "String should match pattern '^(CRITICAL|HIGH|MEDIUM|LOW)$'",
"type": "string_pattern_mismatch"
}
]
}
}
Rate Limiting
When rate limited, wait error.retry_after_seconds (also the Retry-After header) and retry. The per-key limiter also keeps it in error.detail.retry_after. Without either, back off exponentially. A 429 with retryable: false (PARTNER_LIMIT, a used-up trial) will not clear by waiting seconds: see action.
{
"api": { "version": "1.0.0", "request_id": "bs-req-..." },
"error": {
"code": "RATE_LIMITED",
"message": "Per-key limit is 60 requests and 5000 hosts per minute.",
"retryable": true,
"retry_after_seconds": 36,
"action": "retry_later",
"detail": { "error": "rate_limited", "message": "Per-key limit is 60 requests and 5000 hosts per minute.", "retry_after": 36 }
}
}
# Check your current rate limit status
curl -I -H "Authorization: Bearer bs_live_..." \
"https://breachspider.com/api/v1/health"