Errors & Status Codes

Every error response is JSON: { "error": "..." }. Status codes below cover every failure path in the API.

Status Code Reference

StatusMeaningTypical cause
400Bad RequestInvalid 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.
401UnauthorizedMissing or invalid X-Api-Key header. As of 2.0.0 this also applies to /metrics and /health/detail, which previously needed no key.
403ForbiddenKey authenticated but its role lacks permission for this route (e.g. a reader key calling a write endpoint).
404Not FoundCampaign 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.
409ConflictCampaign with that campaign_id already exists.
429Too Many RequestsPer-key rate limit exceeded (BANDITDB_RATE_LIMIT_PER_SEC, default 1000/s). Retry after 1 second.
500Internal Server ErrorScoring, reward, or batch task panicked. Rare — check server logs.
503Service UnavailableWAL 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 for BANDITDB_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 or BANDITDB_MAX_PENDING_INTERACTIONS is 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.

RejectedWhy
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 contextNo 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-finiteMakes every score NaN, or inverts exploration into a penalty on uncertainty.
neural config with a zero dimension or non-finite learning rateBuilds 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.