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.

GET/v1/indexes

List all indexes in your project. An index on a dedicated engine also has instanceId and engineIndexUid.

Auth admin or search keyReturns 200
Response
{
  "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"
    }
  ]
}
POST/v1/indexes

Create a new index with a project-scoped admin key so dashboard, usage, and analytics attribution stay attached.

Auth admin key onlyReturns 202
Request body
{
  "uid": "products",
  "primaryKey": "id",        // optional
  "engine": "meilisearch",   // optional: meilisearch, typesense, elasticsearch or visor
  "instanceId": "<id>"       // optional: one of your dedicated engines
}
Response
{
  "taskUid": 1,
  "indexUid": "products",
  "engine": "meilisearch",
  "status": "enqueued"
}

Per-index engine routing

If 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

Visor indexes run on a Visor dedicated engine. Leave out 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

Pass 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.
GET/v1/indexes/:uid

Get information about a single index. This example is an index on a dedicated engine: other indexes have no instanceId or engineIndexUid.

Auth admin or search keyReturns 200
Response
{
  "uid": "articles",
  "primaryKey": "id",
  "createdAt": "2025-01-16T10:00:00Z",
  "updatedAt": "2025-01-16T10:00:00Z",
  "engine": "typesense",
  "instanceId": "<id>",
  "engineIndexUid": "hbp_<projectId>_articles"
}
DELETE/v1/indexes/:uid

Delete an index and all its documents.

Auth admin key onlyReturns 202
Response
{
  "taskUid": 4,
  "indexUid": "products",
  "status": "enqueued"
}
POST/v1/indexes/:uid/engine

Move 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.

Auth admin key onlyReturns 202
Request body
{
  "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
}
Response
{
  "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
}
GET/v1/indexes/:uid/engine

The index's engine and its latest switch, or null. Poll it while a switch runs: status goes enqueued, processing, then succeeded or failed.

Auth admin key onlyReturns 200
Response
{
  "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

Headband creates the index on the new engine, placed as a new index of that engine on the same branch would be: your 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

Each engine applies a different part of the settings vocabulary (see engine compatibility). A switch carries what the new engine applies and lists every other setting in 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 engineCarriedNot carried
MeilisearchEvery settingNone
VisorSearchable, filterable, sortable and displayed attributes, synonyms, stop words, typo tolerance, built-in ranking rulesCustom ranking rules, distinct attribute, faceting, pagination, token settings, proximity precision, search cutoff, embedders
TypesenseSynonymsAttribute lists (the schema is automatic), stop words, ranking rules, typo tolerance and the rest
ElasticsearchTypo tolerance on or off, distinct attribute, facetingAttribute lists and ranking rules (kept, not applied), synonyms and stop words, the rest

Limits and refusals

An index on your own engine (BYOE) or managed by Sync is not switched. A switch to Typesense needs the primary key 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).