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-search

HTTP 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:

StatusCodeCause
401missing_authorization_headerNo Authorization: Bearer <key> header.
401invalid_api_keyThe key is unknown, revoked or expired.
403permission_deniedA search key on a write: creating an index, or writing or deleting documents.
400invalid_jsonThe body is not valid JSON.
400index_requiredNo index: index in a search body, index= on /v1/documents, indexUid in a multi-search query.
400invalid_requestThe body has the wrong shape, for example documents that are not an array.
400invalid_index_uidCreating an index with no uid, or one that breaks the naming rules.
400invalid_engineCreating an index on an engine name Headband does not know.
400too_many_search_queriesA multi-search with more queries than your plan allows in one call.
400invalid_search_filterA filter on a field that is not filterable, or on Elasticsearch, a field the index does not have.
400invalid_search_facetsA facet on a field that is not filterable, or on Elasticsearch, a text field (use <field>.keyword).
400invalid_search_sortA sort on a field that is not sortable, or on Elasticsearch, a text field (use <field>.keyword) or one the index does not have.
400invalid_search_offsetElasticsearch: offset plus limit past 10,000.
400invalid_search_pagepage is not a whole number of 1 or more. invalid_search_hits_per_page is the same for hitsPerPage.
404index_not_foundNo 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.

503 and 504

Through the REST API, the code on a 503 says why Headband could not serve the request:

CodeMeaningRetry-After
instance_unavailableThe index's dedicated engine is still being created, has failed or is being deleted, or its host is down or cannot be reached. A parked engine is not refused: it wakes.30 when the host is down or marked unreachable. 2 when a request could not reach the host. Otherwise 2 on index create and on the instance API, and none elsewhere.
instance_wakingThe index's dedicated engine is still waking up after it parked.The gateway's value, else 2. 5 on the instance API's wake call.
index_migratingWrites to this index are paused while it moves to another engine. Reads keep working.5
engine_not_configuredHeadband has no engine configured for this request. A retry will not help.None
engine_not_availableThe engine type is not available in that region yet: today, a Visor engine (POST /v1/instances) or a Visor index on your default Visor engine (POST /v1/indexes) where no engine host in the region runs Visor. Nothing was created.None
auth_lookup_failedHeadband could not check your API key. Retry shortly.None

A 503 from the engine itself is passed on as a 503. On Visor, a 503 that may succeed unchanged (storage_unavailable, search_queue_full) carries Retry-After: 1. search_timeout carries none, because the same search would hit the same deadline. On a dedicated engine's own endpoint, the gateway answers 503 instance_waking with Retry-After: 2 when a wake takes longer than the plan's hold (10 seconds on Free, 25 seconds on paid plans), and 503 instance_unavailable when the engine cannot be started or reached. Through the REST API the hold on a paid plan is 30 seconds.

503 instance_unavailable when a host cannot be reached

When the server hosting a dedicated engine cannot be reached, every REST route that uses the engine answers the same way, on Meilisearch, Typesense, Elasticsearch and Visor alike: search, multi-search, documents, indexes, settings and tasks. A refused or dropped connection, a connection that takes more than 5 seconds to open, and a host that answers it does not have the engine loaded yet all give this answer. Answers the engine itself gives, such as a 400 for a bad filter or a 404 for a missing index, are passed on unchanged. An engine with a ready read-only copy on the other server differs for searches and multi-searches. Once its server is marked unreachable, they go straight to the copy. Before that, a failed search is retried once on the copy, so a search can still get this answer. Its other routes always get it.

HTTP/1.1 503 Service Unavailable
Retry-After: 2

{
  "error": "The engine for this index is not available right now. Try again shortly.",
  "code": "instance_unavailable"
}

Headband checks every host from outside every 30 seconds. After three failed checks in a row the host is marked unreachable. Requests for its engines then get this answer at once, with Retry-After: 30, without waiting on the host. The engine shows "state": "unavailable" in GET /v1/instances/<id> and in the dashboard until a check passes or the engine moves. An engine on bucket storage moves to the other server by itself in about 2 to 3 minutes; the Service levels page says which engines have a copy and what happens to recent writes. Retry after the pause; in multi-search, only the queries on that engine fail, each with this code. A write that got this answer after its connection dropped may still have been applied: documents are written by id, so sending them again is safe.

A 504 means the engine did not answer in time. On a dedicated engine's endpoint the gateway answers 504 engine_timeout after 30 seconds without data on a search or read, or after 10 minutes in total for anything else. Through the REST API, a 504 sent by an engine or by a dedicated engine's gateway is passed on as a 504. A write that timed out may still have been applied, so check before you send it again.

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"
}
StatusCodeWhat it means
400api_identity_requiredNever 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.
401missing_credentialsNo 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.
401invalid_credentialsThe 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.
403route_not_allowedThis 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.
403internal_key_requiredNever 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.
404instance_not_foundNo engine answers at this host, or it is being deleted.
405method_not_allowedCONNECT, or a protocol upgrade such as a WebSocket. No engine API needs either.
408request_timeoutThe body of a search or read stopped arriving for 30 seconds before it was complete. Send the request again.
413payload_too_largeThe request body is over 256 MiB. Send it in smaller batches.
429too_many_requestsThe 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.
429too_many_auth_failuresToo many failed authentications from your network address. Wait the seconds in Retry-After.
431request_header_fields_too_largeThe request has 197 or more header fields. Send it with fewer headers.
500internalThe gateway failed while handling the request. Retry it.
502engine_unreachableThe 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.
503instance_busyThe 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.
503lease_expiringThe 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.
503instance_movingA 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.
503instance_fencingA 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.
503instance_read_onlyThe 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.
503instance_wakingThe 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.
503instance_unavailableThe engine could not be started or reached right now, or its host is restarting. Retry after Retry-After when it is sent.
504engine_timeoutThe 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.