Reference
Errors
When a request fails, the API answers with one error body shape, an HTTP status and an error code you can branch on.
Error format
Error responses are JSON with a human-readable error and, for most errors, a machine-readable code. Branch on the code, not the message.
{
"error": "Index `products` not found.",
"code": "index_not_found",
"requestId": "req_3f9c2a7e5b1d4c8a"
}Request IDs
Every /v1 response, success or error, carries an X-Request-Id header, and every JSON error body carries the same value as requestId. Quote it when you contact support: it is how we find your request in our logs and error reports.
To use your own id, send an X-Request-Id header of 8 to 128 characters from A-Z a-z 0-9 . _ : -, such as a UUID or your trace id. Headband echoes it back. Anything else is replaced with a generated req_ id. Browser code can read the header: it is listed in Access-Control-Expose-Headers.
export SEARCH_KEY="hb_src_..."
curl -si \
https://api.headband.dev/v1/search \
-H "Authorization: Bearer $SEARCH_KEY" \
-H "X-Request-Id: 7d1f0c9e-checkout" \
-H "Content-Type: application/json" \
-d '{
"index": "products",
"q": "shoes"
}'
HTTP/2 200
x-request-id: 7d1f0c9e-checkoutHTTP status codes
Request error codes
Errors in the request itself, on the /v1 routes:
The search codes come with a message that names the field and how to set it up. See Search errors.
422 partial_failure
Adding or bulk importing documents on an Elasticsearch or Typesense index, or deleting documents by id on Elasticsearch, returns 422 when the engine rejected some documents and wrote the rest. Only the written documents count toward your usage. Meilisearch writes are asynchronous tasks, so a rejected Meilisearch document shows up in the task (GET /v1/tasks/:taskUid) instead.
Visor writes one batch atomically: a bad document fails the whole request with a 400 naming it, and nothing is written. A batch over the engine's size limit is 413 payload_too_large; send it in smaller batches.
{
"error": "1 of 3 documents failed",
"code": "partial_failure",
"failed": 1,
"errors": [{
"id": "2",
"status": 400,
"type": "mapper_parsing_exception",
"reason": "failed to parse field [price]…",
"retryable": false
}]
}failed counts every rejected document; errors lists at most 100 of them. Each entry has the document id (or null when unknown), the engine's per-document status, a type, a reason, and retryable. A retryable entry (engine status 429 or 5xx) may succeed if you send that document again; a non-retryable one needs the document fixed. Resend only the failed documents: the others were written.
When every failed document is retryable, the body also has "retryable": true and the response a Retry-After: 1 header. On POST /v1/bulk/stream the stream has already answered 200, so the same failed and errors fields arrive on the batch_complete and complete events, and complete carries "code": "partial_failure" when anything failed.
413 payload_too_large
A REST request body can be at most 4 MB (exactly 4,194,304 bytes). Every /v1 route answers a larger body with 413 payload_too_large as JSON, before it reads the body, and nothing is written. The check reads the declared Content-Length: a body sent without one is cut off by the hosting platform at about 4.5 MB, with a plain error and no JSON code. Split large imports into batches under the limit, as the import script does.
{
"error": "Request body is 6 MB, over the…",
"code": "payload_too_large",
"maxBytes": 4194304,
"receivedBytes": 6291456
}502 engine_auth_failed
The search engine refused the credentials Headband holds for it. Your Headband API key is fine; retrying with another key will not help. If your project brings its own engine (BYOE), the host or API key saved in the project's engine settings is wrong, expired or lacks permission: update it in the dashboard. On a Headband-hosted engine it is our problem, and we are alerted.
409 index_managed_by_sync
Sync keeps this index up to date from a Postgres source, so Headband refuses direct writes to it: adding or deleting documents, bulk imports, settings changes, deleting the index, and alias flips. A direct write would be overwritten by the next change on the source. Change the sync definition instead, or delete the synced index and keep the engine index to manage it yourself. See Sync.
429 and Retry-After
A 429 means either Headband's rate limit or the search engine's. Both send a Retry-After header with the seconds to wait when one is known; for the engine's own limit it is passed through as the engine sent it. Wait that long before retrying.
A 429 with code usage_limit_exceeded is different: a Free plan cap was reached, so retrying will not help until the cap resets, you free up room or you upgrade. The caps are monthly searches and stored data; paid plans bill overage instead. At the monthly search cap the body also has resetsAt, and Retry-After is the seconds until then. A plan that slows down at its search cap instead answers usage_throttled to a search over its pace, with a short Retry-After.
A plan allowance answers 402, not 429: plan_project_limit, plan_branch_limit, plan_engine_limit and plan_required. An upgrade raises it. If Headband cannot check stored data or the project limit for a moment, the request answers 503 with code usage_check_unavailable and a Retry-After.
The rate limit on each plan's pricing card (searches and writes a second, code rate_limited) covers POST /v1/search, POST /v1/multi-search, where each query counts, POST /v1/hybrid-search, POST /v1/ai/search and document writes through /v1/documents, and the same searches and writes on an engine's own endpoint. Each key also has its own limit, which follows its plan: the plan's rate a minute, and never fewer than 1,000 searches or 100 writes a minute. Writes there include /v1/bulk, /v1/bulk/stream, settings changes and creating or deleting an index. POST /v1/experiments/events has a fixed 500 a minute.
400 invalid_document_id
A document id in the URL path, as in GET /v1/documents/:docId, may only contain letters, numbers, hyphens and underscores, up to 511 characters. Anything else is rejected before it reaches the engine.
Dedicated engine gateway errors
A dedicated engine's own endpoint sits behind a Headband gateway. When the gateway refuses a request, the body has error, code and message, which repeats error so the engines' official clients show it. A route_not_allowed body also has class. Errors from the engine itself pass through in the engine's own shape.
{
"error": "missing credentials",
"code": "missing_credentials",
"message": "missing credentials"
}See Dedicated engines for the keys, routes and limits behind these codes.