Indexes
Indexes hold your documents and search configuration. Each index has a unique identifier (uid) and an optional primary key that identifies each document. Indexes are automatically scoped to your project. Creating an index through the public API requires a project-scoped admin key; org-scoped keys are for cross-project read/search workflows.
/v1/indexesList all indexes in your project. An index on a dedicated engine also has instanceId and engineIndexUid.
{
"results": [
{
"uid": "products",
"primaryKey": "id",
"createdAt": "2025-01-15T08:30:00Z",
"updatedAt": "2025-01-15T09:12:00Z",
"engine": "meilisearch"
},
{
"uid": "articles",
"primaryKey": "id",
"createdAt": "2025-01-16T10:00:00Z",
"updatedAt": "2025-01-16T10:00:00Z",
"engine": "typesense",
"instanceId": "<id>",
"engineIndexUid": "hbp_<projectId>_articles"
}
]
}/v1/indexesCreate a new index with a project-scoped admin key so dashboard, usage, and analytics attribution stay attached.
{
"uid": "products",
"primaryKey": "id", // optional
"engine": "meilisearch", // optional: meilisearch, typesense, elasticsearch or visor
"instanceId": "<id>" // optional: one of your dedicated engines
}{
"taskUid": 1,
"indexUid": "products",
"engine": "meilisearch",
"status": "enqueued"
}Per-index engine routing
engine is omitted, the project default is used. If it matches the project default, Headband uses that project's custom engine config. If it differs, the index runs on your organization's Headband-hosted engine for that type, on every plan. An index whose engine type has no engine yet gets a default engine made on first use: serverless where the engine has that lane, always on at the smallest size for Elasticsearch. On Free, each engine type gets one engine on your project's main branch, plus one per branch for the engines that park when idle. Create separate projects when you need BYOE configs for multiple engines until per-index custom configs are exposed.Visor indexes
instanceId and the index goes on your default Visor engine, made on first use (serverless, the smallest size) as for Meilisearch. Visor is offered in a region where a dedicated engine host runs it. Where none does, the create answers 503 engine_not_available and nothing is created. Visor runs only on Headband, so it takes no custom engine config: engineConfig with visor answers 400 invalid_engine_config. Writes are synchronous, so a write is searchable when it returns and there is no task to poll. A deployment with no Visor server answers 503 engine_not_configured.On a dedicated engine
instanceId to create the index on one of your dedicated engines. Leave out engine and the index takes that engine's type. Sending null is the same as leaving instanceId out. Every later request for the index goes to that engine. The 202 answer adds instanceId and engineIndexUid, the index's name on the engine. Use that name with the engine's own client. The Dedicated Engines page lists the errors an instanceId can get./v1/indexes/:uidGet information about a single index. This example is an index on a dedicated engine: other indexes have no instanceId or engineIndexUid.
{
"uid": "articles",
"primaryKey": "id",
"createdAt": "2025-01-16T10:00:00Z",
"updatedAt": "2025-01-16T10:00:00Z",
"engine": "typesense",
"instanceId": "<id>",
"engineIndexUid": "hbp_<projectId>_articles"
}/v1/indexes/:uidDelete an index and all its documents.
{
"taskUid": 4,
"indexUid": "products",
"status": "enqueued"
}/v1/indexes/:uid/engineMove an index to another engine: Meilisearch, Typesense, Elasticsearch or Visor. The uid, keys and URLs stay the same, and searches keep working on the old engine until the switch. Needs an admin key with indexes.create and indexes.delete.
{
"engine": "visor", // the new engine
"instanceId": "<id>", // optional: a dedicated engine of the index's branch
"acceptDrops": true, // optional: switch even if some settings will not apply
"declareKey": false, // optional: Visor over 1000 documents, see below
"dryRun": false // optional: answer the plan, change nothing
}{
"switchId": "esw_<id>",
"type": "engineSwitch",
"indexUid": "movies",
"status": "enqueued",
"phase": "queued",
"fromEngine": "meilisearch",
"toEngine": "visor",
"instanceId": "<id>",
"writesHeld": false,
"documents": { "source": 25000, "copied": 0, "verified": null },
"settings": {
"carried": ["searchableAttributes", "filterableAttributes", "synonyms"],
"drops": [],
"added": [{ "setting": "sortableAttributes", "value": "id", "reason": "..." }]
},
"notes": [],
"error": null
}/v1/indexes/:uid/engineThe index's engine and its latest switch, or null. Poll it while a switch runs: status goes enqueued, processing, then succeeded or failed.
{
"indexUid": "movies",
"engine": "meilisearch",
"instanceId": "<id>",
"switch": { "switchId": "esw_<id>", "status": "processing", "phase": "copying" }
}The same switch runs from the dashboard (the index's Change engine) and from the CLI: headband indexes switch-engine products --engine visor --dry-run shows the plan, and without --dry-run (plus --accept-drops when settings would be dropped, and --wait to follow it) it switches.
How a switch runs
instanceId, else the branch's default engine of that type, made on first use within your plan's engines per type. It copies every document while writes still flow (phase copying). Then it holds writes to the index: they answer 503 index_migrating with Retry-After: 5 for about a minute (draining), then while the last changes are copied and every document is checked (catching_up). The minute lets a write that was already on its way to the old engine land there before the last changes are copied, so it is not lost. An index on a shared engine, or a switch while one of Headband's hosts is out of touch, waits five minutes instead, because a write there can take that long to finish. Settings changed during the copy stop the switch with the error settings_changed: start it again to carry them. The switch happens only when both indexes hold the same number of documents, the same ids, and each document as it was sent. Any mismatch stops it: writes resume on the old engine, the new copy is deleted, and nothing changes. After the switch the old copy is deleted (cleaning). Writes sent straight to an engine endpoint with the engine's own client are not held, so pause them during a switch.Settings that do not carry over
settings.drops with a reason. With any drop, the request answers 409 settings_not_portable and the same report until you send acceptDrops: true. A drop with kept: true is stored on the new index but not applied by its search.| New engine | Carried | Not carried |
|---|---|---|
| Meilisearch | Every setting | None |
| Visor | Searchable, filterable, sortable and displayed attributes, synonyms, stop words, typo tolerance, built-in ranking rules | Custom ranking rules, distinct attribute, faceting, pagination, token settings, proximity precision, search cutoff, embedders |
| Typesense | Synonyms | Attribute lists (the schema is automatic), stop words, ranking rules, typo tolerance and the rest |
| Elasticsearch | Typo tolerance on or off, distinct attribute, faceting | Attribute lists and ranking rules (kept, not applied), synonyms and stop words, the rest |
Limits and refusals
id, and copies numeric ids as strings. Typesense types each field by the first value it sees, so a field that mixes whole numbers and decimals would be changed: such a switch stops with content_mismatch and nothing changes. Indexes of any size switch, Elasticsearch ones included. A Visor index over 1000 documents is read by its primary key, which must be filterable and sortable: send declareKey: true to declare it for the read (Visor rebuilds the index), or the request answers 400 primary_key_not_declared. A switch to Visor makes the primary key filterable and sortable there, so the index can move again later. One switch runs per index at a time (409 engine_switch_in_progress).