Errors & Status Codes
Every error response is JSON: { "error": "..." }. Status codes below cover every failure path in the API.
Status Code Reference
| Status | Meaning | Typical cause |
|---|---|---|
400 | Bad Request | Invalid campaign_id/arm_id (must match [a-zA-Z0-9_-]{1,128}), missing arms, bad feature_dim, metadata over 64 KB, or — new in 2.0.0 — an invalid context, reward, alpha, or neural config. See Validation Errors. |
401 | Unauthorized | Missing or invalid X-Api-Key header. As of 2.0.0 this also applies to /metrics and /health/detail, which previously needed no key. |
403 | Forbidden | Key authenticated but its role lacks permission for this route (e.g. a reader key calling a write endpoint). |
404 | Not Found | Campaign doesn't exist, was archived, or the interaction_id sent to /reward has expired. In tenant mode, also returned when the interaction belongs to another tenant — deliberately identical to "unknown", so it does not reveal whether the id exists elsewhere. |
409 | Conflict | Campaign with that campaign_id already exists. |
429 | Too Many Requests | Per-key rate limit exceeded (BANDITDB_RATE_LIMIT_PER_SEC, default 1000/s). Retry after 1 second. |
500 | Internal Server Error | Scoring, reward, or batch task panicked. Rare — check server logs. |
503 | Service Unavailable | WAL writer is unhealthy or full — the engine fast-fails writes rather than risk silent data loss. |
Error Body Shape
{ "error": "Campaign 'checkout' not found" }
Auth Errors (401 / 403)
401 means the X-Api-Key header was missing or didn't match any configured key. 403 means the key is valid but underpowered for the route — e.g. a reader key hitting POST /campaign. Check the key's role against the route's minimum role in the API Reference.
Rate Limiting (429)
Limits are per API key (or per source IP for unauthenticated requests), enforced with a token bucket via governor. A 429 means you've burst past BANDITDB_RATE_LIMIT_PER_SEC — back off and retry after 1 second. Batch requests through POST /batch_predict (up to 100 items) instead of looping individual /predict calls to stay under the limit.
404 on /reward
The most common integration bug: sending a reward for an interaction_id that's no longer known. Two causes:
- The prediction's TTL expired —
interaction_ids live in a cache forBANDITDB_REWARD_TTL_SECS(default 86400s / 24h). Reward within that window. - The cache hit its capacity limit and evicted the prediction. Watch
banditdb_interactions_evicted_total: any increase means rewards are being silently unmatched, and either reward lag has grown orBANDITDB_MAX_PENDING_INTERACTIONSis set too low. - The prediction record was dropped under WAL backpressure — see
banditdb_wal_dropped_total. - In tenant mode, the interaction belongs to a different tenant.
A server restart is not a cause. Predictions awaiting a reward travel inside the checkpoint, so a reward arriving after a restart still matches the prediction that preceded it — see How Recovery Works.
Validation Errors (400)
2.0.0 enforces input rules in the engine rather than in individual route
handlers, so every write path applies them — including
/campaign/:id/interact, which previously bypassed the checks the
other routes performed.
| Rejected | Why |
|---|---|
context containing NaN or infinity |
Propagates into the arm's covariance matrix and cannot be cleared without deleting the campaign. |
context value above BANDITDB_MAX_CONTEXT_MAGNITUDE (default 1e6) |
Finiteness alone is not enough. A value near 1e155 is finite, but the rank-one update squares it — that overflows to infinity, and the resulting NaN is written to the checkpoint and survives restart. |
empty context | No features to score against. |
reward outside [0.0, 1.0], or non-finite |
The confidence bounds and the SNIPS tournament estimator both assume that range. Rescale instead — e.g. revenue / max_revenue. Earlier versions accepted out-of-range values and silently corrupted the arm's bounds. |
alpha negative or non-finite | Makes every score NaN, or inverts exploration into a penalty on uncertainty. |
| neural config with a zero dimension or non-finite learning rate | Builds a degenerate network whose dot products fail on a length mismatch. |
The Python SDK checks the same rules locally and raises ValueError
naming the offending index, so you get the error without a round trip.
503 on Writes
The WAL writer retries transient I/O errors up to 5 times with exponential backoff. If it still can't write, checkpoint() and subsequent writes fast-fail with 503 rather than let state drift out of sync with the log. This is a disk/IO problem on the host, not a client bug — check available disk space and permissions on DATA_DIR.
All 4xx errors are safe to retry after fixing the request. All 5xx errors are safe to retry as-is — BanditDB write paths are designed to fail closed rather than half-apply an update.