What is Headband?#

Headband is a search-as-a-service platform that gives you a single, unified API in front of multiple search engines. Create a project, pick your engine (Meilisearch, Typesense, or Elasticsearch), push documents, and start searching.

Every project gets automatic API keys, built-in CORS support for browser-side search, and a dashboard for managing indexes, documents, and settings. You can switch engines per project without changing any API calls.

Unified API

Same endpoints regardless of which engine you pick.

Instant setup

Sign up, get keys, push documents. No infra to manage.

Zero lock-in

Switch engines anytime. Your API calls stay the same.

Quick Start#

Get up and running in five steps. Replace the example URL with your Headband instance.

1

Create a project in the dashboard

You'll receive an admin key (hb_adm_) and a search key (hb_src_) automatically.

2

Create an index

terminal
curl -X POST https://your-instance.com/v1/indexes \
  -H "Authorization: Bearer hb_adm_YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uid": "products", "primaryKey": "id"}'
3

Push your documents

terminal
curl -X POST https://your-instance.com/v1/documents?index=products&primaryKey=id \
  -H "Authorization: Bearer hb_adm_YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {"id": 1, "title": "Widget", "price": 9.99},
    {"id": 2, "title": "Gadget", "price": 24.99}
  ]'
4

Search

terminal
curl -X POST https://your-instance.com/v1/search \
  -H "Authorization: Bearer hb_src_YOUR_SEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"index": "products", "q": "widget"}'
5

Check task status

terminal
curl https://your-instance.com/v1/tasks/2 \
  -H "Authorization: Bearer hb_adm_YOUR_ADMIN_KEY"

Authentication#

Bearer Header

Include your API key as a Bearer token in the Authorization header on every request:

header
Authorization: Bearer hb_adm_xxxxx...

API Key Types

Headband issues two types of API keys per project. Each key automatically scopes all operations to the project it belongs to.

Admin Key

Prefix: hb_adm_
Full access -- create indexes, push and delete documents, manage settings, and search. Keep this key on your server; never expose it to clients.

Search Key

Prefix: hb_src_
Read-only -- search, list indexes, view settings and stats. Safe for client-side and frontend use.

Permissions

ActionAdmin KeySearch Key
SearchYesYes
List indexesYesYes
Get index infoYesYes
View settingsYesYes
View statsYesYes
List tasksYesYes
Create indexYesNo
Delete indexYesNo
Add/update documentsYesNo
Delete documentsYesNo
Update settingsYesNo

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.

GET
/v1/indexes

List all indexes in your project.

Auth: admin or search key

Returns: 200

Response
{
  "results": [
    {
      "uid": "products",
      "primaryKey": "id",
      "createdAt": "2025-01-15T08:30:00Z",
      "updatedAt": "2025-01-15T09:12:00Z"
    }
  ]
}
POST
/v1/indexes

Create a new index.

Auth: admin key only

Returns: 202

Request body
{
  "uid": "products",
  "primaryKey": "id"   // optional
}
Response
{
  "taskUid": 1,
  "indexUid": "products",
  "status": "enqueued"
}
GET
/v1/indexes/:uid

Get information about a single index.

Auth: admin or search key

Returns: 200

Response
{
  "uid": "products",
  "primaryKey": "id",
  "createdAt": "2025-01-15T08:30:00Z",
  "updatedAt": "2025-01-15T09:12:00Z"
}
DELETE
/v1/indexes/:uid

Delete an index and all its documents.

Auth: admin key only

Returns: 202

Response
{
  "taskUid": 4,
  "indexUid": "products",
  "status": "enqueued"
}

Index Settings#

Configure search behavior for an index. Changes to settings are processed asynchronously and may trigger a re-index of your documents.

GET
/v1/indexes/:uid/settings

Get all settings for an index.

Auth: admin or search key

Returns: 200

Response
{
  "searchableAttributes": ["*"],
  "filterableAttributes": [],
  "sortableAttributes": [],
  "rankingRules": [
    "words", "typo", "proximity",
    "attribute", "sort", "exactness"
  ],
  "synonyms": {},
  "stopWords": [],
  "displayedAttributes": ["*"],
  "distinctAttribute": null,
  "typoTolerance": {
    "enabled": true,
    "minWordSizeForTypos": {
      "oneTypo": 5,
      "twoTypos": 9
    }
  },
  "faceting": {
    "maxValuesPerFacet": 100
  },
  "pagination": {
    "maxTotalHits": 1000
  }
}
PATCH
/v1/indexes/:uid/settings

Update settings for an index. Only include the settings you want to change.

Auth: admin key only

Returns: 202

Request body
{
  "filterableAttributes": ["category", "price"],
  "sortableAttributes": ["price", "createdAt"],
  "searchableAttributes": ["title", "description"]
}
Response
{
  "taskUid": 5,
  "indexUid": "products",
  "status": "enqueued"
}

Available Settings

searchableAttributesstring[]Attributes used for search. Order determines relevance ranking.
filterableAttributesstring[]Attributes that can be used in filter expressions.
sortableAttributesstring[]Attributes that can be used in sort expressions.
rankingRulesstring[]Ordered list of ranking rules applied to search results.
synonymsobjectMap of words to their synonyms, e.g. { "phone": ["mobile", "cell"] }.
stopWordsstring[]Words ignored during search (e.g. "the", "a", "is").
displayedAttributesstring[]Attributes returned in search results. Defaults to all.
distinctAttributestring|nullDe-duplicate results by this attribute.
typoToleranceobjectConfigure typo tolerance behavior and thresholds.
facetingobjectConfigure facet behavior (e.g. maxValuesPerFacet).
paginationobjectConfigure pagination limits (e.g. maxTotalHits).
embeddersobjectConfigure vector embedders for hybrid/semantic search.

Documents#

Documents are JSON objects stored in an index. Headband auto-detects schema from the document structure -- no upfront schema definition required. You can send thousands of documents in a single request for bulk ingestion.

GET
/v1/documents?index=products

List documents in an index with pagination.

Supports limit and offset query parameters for pagination.

Auth: admin or search key

Returns: 200

Response
{
  "results": [
    { "id": 1, "title": "MacBook Pro", "price": 1999 },
    { "id": 2, "title": "iPhone 16", "price": 999 }
  ],
  "total": 19547,
  "limit": 20,
  "offset": 0
}
GET
/v1/documents/:id?index=products

Get a single document by its primary key.

Auth: admin or search key

Returns: 200

Response
{
  "id": 1,
  "title": "MacBook Pro",
  "price": 1999,
  "category": "laptops"
}
POST
/v1/documents?index=products&primaryKey=id

Add or replace documents in an index. If a document with the same primary key already exists, it will be replaced.

Supports bulk upload -- send thousands of documents in one request.

Auth: admin key only

Returns: 202

Request body
[
  { "id": 1, "title": "MacBook Pro", "price": 1999, "category": "laptops" },
  { "id": 2, "title": "iPhone 16", "price": 999, "category": "phones" }
]
Response
{
  "taskUid": 2,
  "indexUid": "products",
  "status": "enqueued",
  "enqueuedAt": "2025-01-15T09:00:00Z"
}
DELETE
/v1/documents?index=products

Delete documents from an index by IDs or by filter.

Auth: admin key only

Returns: 202

Request body
// Delete by IDs:
{ "ids": [1, 2] }

// Or delete by filter:
{ "filter": "price > 1000" }
Response
{
  "taskUid": 3,
  "indexUid": "products",
  "status": "enqueued"
}

Bulk Import

Import large volumes of documents efficiently. The bulk endpoint automatically splits your documents into optimized batches and sends them in parallel, handling payloads of up to 500K documents in a single request.

POST
/v1/bulk

Import documents in optimized parallel batches.

Auth: admin key only

Returns: 202

Request body
{
  "index": "products",
  "primaryKey": "id",
  "documents": [
    { "id": 1, "title": "Widget", "price": 9.99 },
    { "id": 2, "title": "Gadget", "price": 24.99 }
    // ... up to 500K documents
  ]
}
Response
{
  "tasks": [
    { "taskUid": 10, "indexUid": "products", "status": "enqueued", "batchNumber": 1, "documentsInBatch": 10000 },
    { "taskUid": 11, "indexUid": "products", "status": "enqueued", "batchNumber": 2, "documentsInBatch": 10000 }
  ],
  "totalDocuments": 20000,
  "totalBatches": 2,
  "batchSize": 10000
}

Parameters

batchSizenumber10000Documents per batch (min 100, max 50000).

Streaming Progress

POST
/v1/bulk/stream

Import documents with real-time Server-Sent Events progress. Same request body as /v1/bulk.

Use the streaming endpoint for large imports to get real-time progress feedback. Combine with the Tasks API to poll individual batch status after import.

Auth: admin key only

Returns: 200 (SSE stream)

Request body
{
  "index": "products",
  "primaryKey": "id",
  "documents": [
    { "id": 1, "title": "Widget", "price": 9.99 },
    { "id": 2, "title": "Gadget", "price": 24.99 }
    // ... up to 500K documents
  ]
}
Response
data: {"event":"batch_complete","batchNumber":1,"taskUid":10,"documentsInBatch":10000}

data: {"event":"batch_complete","batchNumber":2,"taskUid":11,"documentsInBatch":10000}

data: {"event":"complete","totalDocuments":20000,"totalBatches":2}

Tasks#

Many operations (index creation, document ingestion, settings updates) are processed asynchronously. These operations return a taskUid you can poll to track progress.

GET
/v1/tasks

List all tasks for your project.

Auth: admin or search key

Returns: 200

Response
{
  "results": [
    {
      "uid": 1,
      "indexUid": "products",
      "status": "succeeded",
      "type": "indexCreation",
      "enqueuedAt": "2025-01-15T08:30:00Z",
      "startedAt": "2025-01-15T08:30:01Z",
      "finishedAt": "2025-01-15T08:30:01Z"
    },
    {
      "uid": 2,
      "indexUid": "products",
      "status": "processing",
      "type": "documentAdditionOrUpdate",
      "enqueuedAt": "2025-01-15T09:00:00Z",
      "startedAt": "2025-01-15T09:00:01Z"
    }
  ]
}

Parameters

limitnumber20Number of tasks to return.
fromnumber--Task UID to start from (for pagination).
statusesstring--Comma-separated list of statuses to filter (e.g. "succeeded,failed").
typesstring--Comma-separated list of task types to filter.
indexUidsstring--Comma-separated list of index UIDs to filter.
GET
/v1/tasks/:taskId

Get the status and details of a single task.

Task statuses: enqueued, processing, succeeded, failed.

Auth: admin or search key

Returns: 200

Response
{
  "uid": 2,
  "indexUid": "products",
  "status": "succeeded",
  "type": "documentAdditionOrUpdate",
  "details": {
    "receivedDocuments": 1500,
    "indexedDocuments": 1500
  },
  "enqueuedAt": "2025-01-15T09:00:00Z",
  "startedAt": "2025-01-15T09:00:01Z",
  "finishedAt": "2025-01-15T09:00:03Z"
}

React SDK#

@headband/react provides InstantSearch-compatible hooks and pre-built components for building search UIs with React. It handles debouncing, state management, facet filtering, pagination, and more out of the box.

Installation

terminal
npm install @headband/react

HeadbandProvider

Wrap your search UI in a HeadbandProvider. It creates a client, manages search state, and provides context to all child hooks and components.

tsx
import { headband, HeadbandProvider } from "@headband/react";

const client = headband("https://your-instance.com", "hb_src_YOUR_SEARCH_KEY");

function App() {
  return (
    <HeadbandProvider client={client} index="products">
      {/* Search components go here */}
    </HeadbandProvider>
  );
}

Parameters

clientHeadbandClientRequiredClient created with headband(host, apiKey).
indexstringRequiredThe index UID to search.
initialQuerystring""Optional initial search query.
childrenReactNodeRequiredChild components that use Headband hooks.

Hooks

useSearch()

Core hook exposing the full search state and all actions. Use the specialized hooks below for most cases.

const { query, results, isLoading, error, setQuery, setPage, refresh } = useSearch();
useSearchBox()

Binds to the search input. Returns the current query plus setters.

const { query, setQuery, clear } = useSearchBox();

// query: string        - Current query
// setQuery(q: string)  - Update query (resets pagination, triggers debounced search)
// clear()              - Reset query to ""
useHits<T>()

Returns the current search hits with loading and error state. Supports generics for typed hits.

const { hits, isLoading, error } = useHits<Product>();

// hits: Hit<T>[]   - Each hit has document fields + _formatted + __data
// isLoading: boolean
// error: Error | null
useRefinementList(props)

Provides facet filtering for a given attribute. Automatically registers the facet with the provider.

const { items, refine, canToggle, isLoading } = useRefinementList({
  attribute: "category",
  limit: 20,          // max facet values (default: 20)
  sortBy: "count",    // "count" or "alpha" (default: "count")
});

// items: { value: string, count: number, isRefined: boolean }[]
// refine(value: string)  - Toggle a facet value on/off
usePagination()

Pagination state derived from the current search results.

const { currentPage, totalPages, totalHits, setPage, canPrevious, canNext, pages } = usePagination();

// pages: number[]   - Window of page numbers for rendering a page list
// Pages are 0-indexed internally
useStats()

Returns summary stats about the current search results.

const { totalHits, processingTimeMs, query, isLoading } = useStats();
useSortBy({ items })

Sort-by dropdown state. Pass sort options, get back the current selection.

const { currentSort, setSort, items } = useSortBy({
  items: [
    { value: "price:asc", label: "Price (low to high)" },
    { value: "price:desc", label: "Price (high to low)" },
  ],
});

// items[n].isSelected: boolean  - Added to each item
// setSort(null)                 - Reset to default relevance
useRange({ attribute })

Numeric range filter for a given attribute.

const { min, max, stats, setRange, clear, isLoading } = useRange({ attribute: "price" });

// stats: { min: number, max: number } | undefined  - Overall min/max from engine
// setRange({ min: 10, max: 500 })   - Set range filter
// clear()                            - Remove range filter
useHighlight({ hit, attribute })

Extracts the highlighted value for a specific attribute from a hit. Sanitizes HTML -- only allows <em> tags.

const { value, highlighted } = useHighlight({ hit, attribute: "title" });

// value: string       - Safe HTML string with <em> tags
// highlighted: boolean - Whether the value contains highlight marks

// Usage: <span dangerouslySetInnerHTML={{ __html: value }} />

Components

Pre-built UI components with default Tailwind styles. Set styled=false for unstyled variants.

<SearchBox />

Search input with built-in clear button and keyboard handling.

Props: placeholder, className, autoFocus, onQueryChange, styled

<Hits />

Renders search result hits. Pass a custom hitComponent for custom rendering.

Props: hitComponent, className, emptyComponent, styled

<RefinementList />

Checkbox list for facet filtering on a given attribute.

Props: attribute, limit, sortBy, className, styled

<Pagination />

Page navigation with previous/next buttons and page number list.

Props: className, styled

<Stats />

Displays result count and processing time (e.g. "42 results in 2ms").

Props: className, styled

<SortBy />

Dropdown to switch sort order.

Props: items, className, styled

<Highlight />

Renders highlighted text for a given hit attribute.

Props: hit, attribute, className

<RangeInput />

Min/max numeric input fields for range filtering.

Props: attribute, className, styled

<PoweredBy />

"Powered by Headband" attribution badge.

Props: className, styled

Full Example

A complete product search page with facets, sorting, pagination, and highlighting.

tsx
import {
  headband,
  HeadbandProvider,
  SearchBox,
  Hits,
  RefinementList,
  Pagination,
  Stats,
  SortBy,
  Highlight,
} from "@headband/react";

const client = headband(
  "https://your-instance.com",
  "hb_src_YOUR_SEARCH_KEY"
);

function ProductHit({ hit }) {
  return (
    <div className="p-4 border rounded-lg">
      <Highlight hit={hit} attribute="title" />
      <p className="text-sm text-gray-500">${hit.price}</p>
    </div>
  );
}

export default function SearchPage() {
  return (
    <HeadbandProvider client={client} index="products">
      <div className="max-w-5xl mx-auto p-6">
        <SearchBox placeholder="Search products..." autoFocus />
        <div className="flex gap-8 mt-6">
          <aside className="w-48 shrink-0">
            <h3 className="text-sm font-semibold mb-2">Category</h3>
            <RefinementList attribute="category" />
            <h3 className="text-sm font-semibold mt-4 mb-2">Sort</h3>
            <SortBy
              items={[
                { value: "price:asc", label: "Price: Low to High" },
                { value: "price:desc", label: "Price: High to Low" },
              ]}
            />
          </aside>
          <main className="flex-1">
            <Stats />
            <Hits hitComponent={ProductHit} />
            <Pagination />
          </main>
        </div>
      </div>
    </HeadbandProvider>
  );
}

Engine Compatibility#

Headband supports Meilisearch, Typesense, and Elasticsearch backends. The API is engine-agnostic -- the same endpoints work regardless of which engine your project uses.

When you create a project, you select the engine type and provide a connection URL. All subsequent API calls are translated to the appropriate engine protocol automatically.

The Elasticsearch engine is also compatible with OpenSearch. Point your connection at any OpenSearch node and Headband will work the same way.

Feature Matrix

FeatureMeilisearchTypesenseElasticsearch
Full-text searchYesYesYes
Typo toleranceYesYesYes
Faceted searchYesYesYes
FilteringYesYesYes
SortingYesYesYes
HighlightingYesYesYes
PaginationYesYesYes
SynonymsYesYesYes
Stop wordsYesYesYes
Geo searchYesYesYes
Hybrid / vector searchYesNoYes
Multi-searchYesYesYes

CORS: The /v1 API supports CORS for browser-based usage. Search keys are safe to use in frontend applications. Admin keys should only be used server-side.

Errors#

Error Format

All error responses follow a consistent JSON structure:

{
  "error": "Index not found: products"
}

HTTP Status Codes

200OK -- request succeeded.
202Accepted -- task enqueued for async processing.
400Bad Request -- invalid parameters or body.
401Unauthorized -- missing or invalid 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.
500Internal Server Error -- something went wrong on our end.