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.
Create a project in the dashboard
You'll receive an admin key (hb_adm_) and a search key (hb_src_) automatically.
Create an index
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"}'Push your documents
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}
]'Search
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"}'Check task status
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:
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
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.
/v1/indexesList all indexes in your project.
Auth: admin or search key
Returns: 200
{
"results": [
{
"uid": "products",
"primaryKey": "id",
"createdAt": "2025-01-15T08:30:00Z",
"updatedAt": "2025-01-15T09:12:00Z"
}
]
}/v1/indexesCreate a new index.
Auth: admin key only
Returns: 202
{
"uid": "products",
"primaryKey": "id" // optional
}{
"taskUid": 1,
"indexUid": "products",
"status": "enqueued"
}/v1/indexes/:uidGet information about a single index.
Auth: admin or search key
Returns: 200
{
"uid": "products",
"primaryKey": "id",
"createdAt": "2025-01-15T08:30:00Z",
"updatedAt": "2025-01-15T09:12:00Z"
}/v1/indexes/:uidDelete an index and all its documents.
Auth: admin key only
Returns: 202
{
"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.
/v1/indexes/:uid/settingsGet all settings for an index.
Auth: admin or search key
Returns: 200
{
"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
}
}/v1/indexes/:uid/settingsUpdate settings for an index. Only include the settings you want to change.
Auth: admin key only
Returns: 202
{
"filterableAttributes": ["category", "price"],
"sortableAttributes": ["price", "createdAt"],
"searchableAttributes": ["title", "description"]
}{
"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.
/v1/documents?index=productsList documents in an index with pagination.
Supports limit and offset query parameters for pagination.
Auth: admin or search key
Returns: 200
{
"results": [
{ "id": 1, "title": "MacBook Pro", "price": 1999 },
{ "id": 2, "title": "iPhone 16", "price": 999 }
],
"total": 19547,
"limit": 20,
"offset": 0
}/v1/documents/:id?index=productsGet a single document by its primary key.
Auth: admin or search key
Returns: 200
{
"id": 1,
"title": "MacBook Pro",
"price": 1999,
"category": "laptops"
}/v1/documents?index=products&primaryKey=idAdd 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
[
{ "id": 1, "title": "MacBook Pro", "price": 1999, "category": "laptops" },
{ "id": 2, "title": "iPhone 16", "price": 999, "category": "phones" }
]{
"taskUid": 2,
"indexUid": "products",
"status": "enqueued",
"enqueuedAt": "2025-01-15T09:00:00Z"
}/v1/documents?index=productsDelete documents from an index by IDs or by filter.
Auth: admin key only
Returns: 202
// Delete by IDs:
{ "ids": [1, 2] }
// Or delete by filter:
{ "filter": "price > 1000" }{
"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.
/v1/bulkImport documents in optimized parallel batches.
Auth: admin key only
Returns: 202
{
"index": "products",
"primaryKey": "id",
"documents": [
{ "id": 1, "title": "Widget", "price": 9.99 },
{ "id": 2, "title": "Gadget", "price": 24.99 }
// ... up to 500K documents
]
}{
"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
/v1/bulk/streamImport 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)
{
"index": "products",
"primaryKey": "id",
"documents": [
{ "id": 1, "title": "Widget", "price": 9.99 },
{ "id": 2, "title": "Gadget", "price": 24.99 }
// ... up to 500K documents
]
}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}Search#
Full-text search with support for filters, faceted search, sorting, highlighting, and hybrid vector search.
/v1/searchSearch an index.
Auth: admin or search key
Returns: 200
{
"index": "products",
"q": "macbook",
"limit": 10,
"offset": 0,
"filter": "price < 2000",
"sort": ["price:asc"],
"facets": ["category"],
"attributesToHighlight": ["title"],
"attributesToRetrieve": ["id", "title", "price"]
}{
"hits": [
{
"id": 1,
"title": "MacBook Pro",
"price": 1999,
"_formatted": {
"title": "<em>MacBook</em> Pro"
}
}
],
"query": "macbook",
"processingTimeMs": 2,
"estimatedTotalHits": 42,
"facetDistribution": {
"category": { "laptops": 12, "phones": 30 }
}
}Parameters
indexstringRequiredThe index uid to search.qstring""The search query string. Empty string returns all documents.limitnumber10Maximum number of hits to return.offsetnumber0Number of hits to skip (for pagination).filterstring""Filter expression, e.g. "price < 2000 AND category = laptops".sortstring[][]Sort order, e.g. ["price:asc", "title:desc"].facetsstring[][]Attributes to compute facet distributions for.attributesToHighlightstring[][]Attributes to wrap matches with <em> tags.attributesToRetrievestring[]["*"]Attributes to include in each hit. Defaults to all.attributesToCropstring[][]Attributes whose values will be cropped around matches.cropLengthnumber10Number of words around a match when cropping.showMatchesPositionbooleanfalseInclude match position information in each hit.showRankingScorebooleanfalseInclude the ranking score in each hit.matchingStrategystring"last""last", "all", or "frequency".hybridobjectnullEnable hybrid search. Example: { semanticRatio: 0.5, embedder: "default" }.vectornumber[]nullVector for nearest-neighbor search (used with hybrid or standalone).Filters
Filter expressions let you narrow search results. Attributes must be listed in filterableAttributes in your index settings before they can be used in filters.
// Simple comparison
"price < 2000"
// Equality
"category = laptops"
// Combined with AND / OR
"price < 2000 AND category = laptops"
"category = laptops OR category = phones"
// IN operator
"category IN [laptops, phones, tablets]"
// Negation
"NOT category = accessories"
// Nested with parentheses
"(price < 500 OR price > 2000) AND category = laptops"Facets
Pass attribute names in the facets array to receive a distribution of values for each attribute. Useful for building filter UIs with counts.
"facetDistribution": {
"category": {
"laptops": 12,
"phones": 30,
"tablets": 8
}
}Sorting
Sort results by one or more attributes. Attributes must be listed in sortableAttributes. Use the format attribute:direction where direction is asc or desc.
"sort": ["price:asc", "title:desc"]Highlighting
Request highlighted versions of attributes to show which parts of the text matched the query. Matched text is wrapped in <em> tags.
"_formatted": {
"title": "<em>MacBook</em> Pro 16-inch"
}Multi-Search#
Execute multiple search queries in a single request. Each query targets an index and accepts the same parameters as /v1/search, using indexUid instead of index.
/v1/multi-searchSearch multiple indexes in a single request.
Each query in the queries array accepts all parameters from the search endpoint, using indexUid instead of index.
Auth: admin or search key
Returns: 200
{
"queries": [
{
"indexUid": "products",
"q": "macbook",
"limit": 5
},
{
"indexUid": "articles",
"q": "macbook review",
"limit": 3,
"attributesToHighlight": ["title"]
}
]
}{
"results": [
{
"indexUid": "products",
"hits": [ ... ],
"query": "macbook",
"processingTimeMs": 1,
"estimatedTotalHits": 12
},
{
"indexUid": "articles",
"hits": [ ... ],
"query": "macbook review",
"processingTimeMs": 2,
"estimatedTotalHits": 5
}
]
}Tasks#
Many operations (index creation, document ingestion, settings updates) are processed asynchronously. These operations return a taskUid you can poll to track progress.
/v1/tasksList all tasks for your project.
Auth: admin or search key
Returns: 200
{
"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./v1/tasks/:taskIdGet the status and details of a single task.
Task statuses: enqueued, processing, succeeded, failed.
Auth: admin or search key
Returns: 200
{
"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
npm install @headband/reactHeadbandProvider
Wrap your search UI in a HeadbandProvider. It creates a client, manages search state, and provides context to all child hooks and components.
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 | nulluseRefinementList(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/offusePagination()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 internallyuseStats()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 relevanceuseRange({ 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 filteruseHighlight({ 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.
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
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.