Errors
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_3f9c2a7e5b1d4c8a9e0f6b2d7c4a1e93"
}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.
curl -si https://headband.dev/v1/search \
-H "Authorization: Bearer $HEADBAND_SEARCH_KEY" \
-H "X-Request-Id: 7d1f0c9e-checkout-search" \
-H "Content-Type: application/json" \
-d '{"index": "products", "q": "shoes"}'
HTTP/2 200
x-request-id: 7d1f0c9e-checkout-searchHTTP Status Codes
200OK: request succeeded.202Accepted: task enqueued for async processing.400Bad Request: invalid parameters or body.401Unauthorized: missing or invalid Headband API key.403Forbidden: key does not have permission for this action.404Not Found: index or resource does not exist.405Method Not Allowed: HTTP method not supported for this endpoint.413Payload Too Large: the request body is over the size limit.422Unprocessable: a write where some documents were rejected (partial_failure).429Too Many Requests: rate limited, or a plan limit reached.500Internal Server Error: something went wrong on our end.502Bad Gateway: the search engine rejected Headband's credentials.503Service Unavailable: Headband or the engine cannot take the request right now. Retry after Retry-After when it is sent.504Gateway Timeout: the engine did not answer in time.Request Error Codes
Errors in the request itself, on the /v1 routes:
| Status | Code | Cause |
|---|---|---|
| 401 | missing_authorization_header | No Authorization: Bearer <key> header. |
| 401 | invalid_api_key | The key is unknown, revoked or expired. |
| 403 | permission_denied | A search key on a write: creating an index, or writing or deleting documents. |
| 400 | invalid_json | The body is not valid JSON. |
| 400 | index_required | No index: index in a search body, index= on /v1/documents, indexUid in a multi-search query. |
| 400 | invalid_request | The body has the wrong shape, for example documents that are not an array. |
| 400 | invalid_index_uid | Creating an index with no uid, or one that breaks the naming rules. |
| 400 | invalid_engine | Creating an index on an engine name Headband does not know. |
| 400 | too_many_search_queries | A multi-search with more queries than your plan allows in one call. |
| 400 | invalid_search_filter | A filter on a field that is not filterable, or on Elasticsearch, a field the index does not have. |
| 400 | invalid_search_facets | A facet on a field that is not filterable, or on Elasticsearch, a text field (use <field>.keyword). |
| 400 | invalid_search_sort | A sort on a field that is not sortable, or on Elasticsearch, a text field (use <field>.keyword) or one the index does not have. |
| 400 | invalid_search_offset | Elasticsearch: offset plus limit past 10,000. |
| 400 | invalid_search_page | page is not a whole number of 1 or more. invalid_search_hits_per_page is the same for hitsPerPage. |
| 404 | index_not_found | No index with that uid is visible to the key. In multi-search, the failing query carries it in its own result. |
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] of type [long]",
"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 MiB (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 4 MB limit. Split the documents into smaller batches of under 4 MB each (about 2 for this one) and send them one request at a time.",
"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 plan limit was reached, so retrying will not help until the limit resets, you free up room or you upgrade. On the Free plan the limits are searches, stored data and projects. Paid plans bill overage for searches and stored data instead, so at most their project limit can return it. At the monthly search cap the body also has resetsAt, and Retry-After is the seconds until then.
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, and document writes through /v1/documents. Other routes have a fixed limit per key instead, the same on every plan: for example 100 requests a minute shared by /v1/bulk, /v1/bulk/stream, document writes, settings changes and creating or deleting an index, 300 a minute for POST /v1/hybrid-search, 500 a minute for POST /v1/experiments/events, and 1,000 a minute for POST /v1/ai/search, which shares one budget with search and multi-search.
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"
}| Status | Code | What it means |
|---|---|---|
| 400 | api_identity_required | Never for a request through your engine endpoint. The gateway sends it only on the box's API router socket, when the router forwards a request without naming the organization, key and plan it checked. |
| 401 | missing_credentials | No key in a header the gateway reads. A key in the query string is ignored, so ?x-typesense-api-key= on its own lands here too. Typesense's SearchClient sends that by default: create it with sendApiKeyAsQueryParam: false. |
| 401 | invalid_credentials | The key is unknown, expired or revoked, belongs to another organization, or cannot reach this engine: a project key belongs to another project or branch, or the key is limited to other engines. |
| 403 | route_not_allowed | This key cannot call this route. The body's class names the route class: search, read, write, manage or platform. Platform routes, and paths the gateway does not know, are closed to every key. |
| 403 | internal_key_required | Never for a request through your engine endpoint. The gateway sends it only on the box's internal socket, to the box's own Sync service when it calls without an internal key. |
| 404 | instance_not_found | No engine answers at this host, or it is being deleted. |
| 405 | method_not_allowed | CONNECT, or a protocol upgrade such as a WebSocket. No engine API needs either. |
| 408 | request_timeout | The body of a search or read stopped arriving for 30 seconds before it was complete. Send the request again. |
| 413 | payload_too_large | The request body is over 256 MiB. Send it in smaller batches. |
| 429 | too_many_requests | The engine already has your plan's limit of requests in flight (4 on Free, 8 on Starter, 32 on Pro, 128 on Scale), counting requests that wait for a wake. Retry after Retry-After, which is 2 seconds. |
| 429 | too_many_auth_failures | Too many failed authentications from your network address. Wait the seconds in Retry-After. |
| 431 | request_header_fields_too_large | The request has 197 or more header fields. Send it with fewer headers. |
| 500 | internal | The gateway failed while handling the request. Retry it. |
| 502 | engine_unreachable | The gateway could not reach the engine, or lost it partway through the request. For a write, the outcome is unknown: the engine may have applied it. Check before you send it again, for example by reading the document back. An Elasticsearch write without an explicit id, such as POST /{index}/_doc, stores a second copy if it is sent again, and the official Elasticsearch and Typesense clients retry a 502 by default. Give writes explicit ids, so a repeat overwrites the document instead of adding another. |
| 503 | instance_busy | The engine is busy, for example finishing a backup, and could not take the request in time. Retry after the seconds in Retry-After: 2, unless the engine host sends its own. |
| 503 | lease_expiring | The engine host is renewing its hold on the engine's storage, which takes a few seconds. The body's error reads instance storage lease is being renewed, retry shortly. Retry after Retry-After, which is 2 seconds. |
| 503 | instance_moving | A write waited while the engine moved to a new size, and the move took longer than the wait. Nothing was sent to the engine, so sending it again is safe. Retry after Retry-After, which is 2 seconds. |
| 503 | instance_fencing | A write (anything but a search or a read) was refused before it was sent to the engine, while its host was handing off or renewing the engine's storage. Nothing was applied, so sending it again is safe. The body's error reads instance storage is being fenced; the write was not acknowledged, retry shortly. Retry after Retry-After, which is 2 seconds. A write cut after it reached the engine gets a 502 engine_unreachable instead, and one cut after its answer had started arrives as a dropped connection. In both cases the outcome is unknown. |
| 503 | instance_read_only | The engine's host cannot reach Headband's control plane, so it takes no writes. Searches and reads keep working. The write was refused before it reached the engine, so send it again. Writes resume by themselves once the control plane is back. Task status reads, Meilisearch's /tasks and /batches and Elasticsearch's _tasks, get this answer too. The body's error reads the control plane cannot be reached; searches work and writes resume by themselves. Retry after Retry-After, which is 10 seconds. |
| 503 | instance_waking | The engine took longer to wake than your plan's hold: 10 seconds on Free, 25 seconds on paid plans. A request through the Headband REST API waits up to 30 seconds on a paid plan. Retry after Retry-After, which is 2 seconds. An engine loading a large index after a restart, resize, wake or move answers this code at once, without the hold, once its start has outlasted the hold. Its body says instance is starting and carries starting: when the start began, and usualS, how long a start at this size usually takes. Its Retry-After is the usual time left, from 5 to 60 seconds. A Typesense engine with 700,000 vectors takes about 25 seconds, or about 16 minutes when it has to rebuild its vector index. See start times. |
| 503 | instance_unavailable | The engine could not be started or reached right now, or its host is restarting. Retry after Retry-After when it is sent. |
| 504 | engine_timeout | The engine took too long: 30 seconds without data for a search or read, or 10 minutes in total for anything else. |
See Dedicated Engines for the keys, routes and limits behind these codes.