> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Product Search

> Search inside applications and websites. The endpoint, keys, errors and limits for each customer's dedicated deployment.

> **Note**
>
> Product Search is in beta with design partners. Endpoints roll out per tenant.

Captain Product Search powers the search box inside an application or website: product search on a store, listing search on a marketplace, article search on a help center, or record search inside a SaaS product.

Captain File Search is a separate product for files and their contents. It uses collections, documents and chunks. Product Search uses indexes and records.

## Vocabulary

| Term                 | Meaning                                                                                                           |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Tenant               | One customer's dedicated Product Search deployment, with its own endpoint.                                        |
| Index                | A searchable set of records, such as `products`, `stores` or `articles`.                                          |
| Record               | One product, article, listing or other entity in an index.                                                        |
| Query, filter, facet | The usual search terms: the text searched, the conditions every result must meet, and the counts per field value. |

## How a request finds its data

Each customer gets a dedicated deployment and a dedicated hostname, for example `https://acme.captain.dev`.

* The hostname selects the tenant.
* The path selects the index: `/v1/indexes/products/search`.
* The key grants access to that tenant's indexes and operations.

Requests carry no tenant id, and there is no `/tenants/...` path. Two tenants may each have an index called `products`. A key issued for one tenant does not work against another tenant's endpoint.

## What Captain manages

Captain creates each tenant and its keys, and tunes ranking and index settings with the customer. The customer's application creates indexes, keeps their records up to date, searches, and sends click and conversion events. [Keys](#keys) lists which key calls which operation.

## Endpoint

Onboarding provides two things: the tenant endpoint, for example `https://acme.captain.dev`, and the keys for this tenant. The [Quickstart](/guides/product-search/quickstart) creates a first index from there.

Every Product Search route sits under that endpoint:

```
https://acme.captain.dev/v1/indexes/products/search
https://acme.captain.dev/v1/indexes/articles/search
```

## Keys

| Key        | Use it for                                                                                                                                                                                                                                          | Where it lives                                                                     |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Search key | Searching, reading records, sending events and reading analytics.                                                                                                                                                                                   | Server side. It reads your analytics, which include what your shoppers search for. |
| Scoped key | Searching, reading records and sending events from a browser or an app. Restrict it to your store's origins with `allowed_origins`. On an index with per-record access control, mint a short-lived one per signed-in user. It can't read analytics. | Safe in a browser, once minted.                                                    |
| Issuer key | Minting short-lived scoped keys, for example one per signed-in user. It can't search.                                                                                                                                                               | Server side only.                                                                  |
| Write key  | Creating, listing, copying and deleting indexes and aliases, changing settings, synonyms and rules, adding, updating and deleting records, importing, reading and cancelling jobs, and managing keys. It cannot search.                             | Server side only.                                                                  |

Captain issues a search key, a write key and an issuer key at onboarding, each for every index, and keeps the admin key. Keep all three on your server. In a browser, use a scoped key restricted to your store's origins, as the [Search widget](/guides/product-search/search-widget#get-a-browser-key) guide shows. Access-control and audit settings go through Captain. [Index settings](/guides/product-search/settings) lists what you set and what Captain sets.

Only a search key reads analytics. A scoped, issuer or write key gets `403`. On an index with per-record access control, analytics need Captain's admin key, so Captain provides them.

### Which key calls which operation

| Operation                                                         | Search key | Scoped key | Issuer key       | Write key                                                                                                                 |
| ----------------------------------------------------------------- | ---------- | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Search, typeahead, browse, similar, multiple searches             | Yes        | Yes        | No               | No                                                                                                                        |
| Get records                                                       | Yes        | Yes        | No               | No                                                                                                                        |
| List filters                                                      | Yes        | Yes        | No               | No                                                                                                                        |
| Send events                                                       | Yes        | Yes        | No               | No                                                                                                                        |
| Read analytics                                                    | Yes        | No         | No               | No                                                                                                                        |
| Create an index                                                   | No         | No         | No               | Yes, with a write key for every index                                                                                     |
| List indexes                                                      | No         | No         | No               | Yes                                                                                                                       |
| Add, replace, update and delete records                           | No         | No         | No               | Yes                                                                                                                       |
| Delete records by filter, empty an index                          | No         | No         | No               | Yes                                                                                                                       |
| Get an index's status and update status                           | No         | No         | No               | Yes                                                                                                                       |
| Import from Algolia                                               | No         | No         | No               | Yes. Into a new index, with a write key for every index.                                                                  |
| List jobs, get a job, cancel a queued job                         | No         | No         | No               | Yes                                                                                                                       |
| Change settings, synonyms and rules                               | No         | No         | No               | Yes, except `acl`, `tenant_field` and `audit`                                                                             |
| Set and delete aliases, split shoppers between copies of an index | No         | No         | No               | Yes                                                                                                                       |
| Copy an index                                                     | No         | No         | No               | Yes, with a write key for every index                                                                                     |
| Create keys                                                       | No         | No         | Scoped keys only | Yes, with a write key for every index                                                                                     |
| List, get and revoke keys                                         | No         | No         | No               | Yes, with a write key for every index                                                                                     |
| Read usage                                                        | No         | No         | No               | Yes                                                                                                                       |
| Delete an index                                                   | No         | No         | No               | Yes. Keys limited to that index alone are revoked. See [Delete an index](/guides/product-search/records#delete-an-index). |

A tenant's keys work on every index in that tenant, including indexes created later. A key can also be limited to some indexes. A request for any other index returns `403`, the same answer as for an index that does not exist.

### Manage your keys

The write key you got at onboarding covers every index, and it manages keys too. Create Key mints a search, scoped, write or issuer key, for every index (`["*"]`) or for the indexes you name. The new key's secret is in that reply only. Captain stores a hash and can't show it again.

Create Key takes a write key for every index or an issuer key. Any other key gets `403`.

```bash
curl -X POST "https://acme.captain.dev/v1/keys" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "search", "indexes": ["products-staging"], "description": "staging storefront"}'
```

**`Example: (201 Created)`**

```json Example: (201 Created)
{
  "id": "key_8a1f3c0d9e2b4f67",
  "type": "search",
  "indexes": ["products-staging"],
  "prefix": "cap_k3Jd9x",
  "description": "staging storefront",
  "created_by": "key_2c7e90a1b4d35f18",
  "created_at": 1759433001,
  "key": "cap_k3Jd9x...",
  "note": "store this key now: it is shown once and kept only as a hash"
}
```

A key can also carry `expires_at` (unix seconds) and `allowed_origins`, the sites a browser may use it from.

#### Mint a scoped key with the issuer key

The issuer key you got at onboarding does one thing: it mints scoped keys. Use it on your server to give each visitor or signed-in user a short-lived key. A key it mints must be `scoped`, must name its indexes, and must carry an `expires_at` within the issuer's `max_ttl` (one hour unless it was set otherwise). The issuer's own filter is added to every key it mints. Its `allowed_origins` carry over too, unless the request sends its own.

```bash
curl -X POST "https://acme.captain.dev/v1/keys" \
  -H "Authorization: Bearer $CAPTAIN_ISSUER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "scoped", "indexes": ["products"], "filter": {"status": "published"}, "expires_at": '"$(( $(date +%s) + 3600 ))"'}'
```

`expires_at` here is one hour from now, the longest an issuer key allows by default.

The reply has the same shape as above, with `issued_by` naming the issuer key. Revoking the issuer key revokes every key it minted.

#### List and revoke keys

List Keys (`GET /v1/keys`) returns every key without its secret, and marks the key making the call with `current: true`. Get Key (`GET /v1/keys/{key_id}`) returns one key. Revoke Key (`DELETE /v1/keys/{key_id}`) stops a key at once.

Revoking a key also revokes every key it created, and every key those created. The reply's `revoked` lists every id it revoked. To revoke only the one key, send `?cascade=none`.

A key can't revoke itself, so you can't lock yourself out. Revoking the key that makes the call answers `409 KEY_IS_CALLER`. To replace a leaked write key:

1. Get a new write key for every index that the old key didn't create: ask Captain support for one, or mint it with another write key you trust. A key the old key minted would be revoked with it, so revoking the old key with such a key answers `409 CASCADE_INCLUDES_CALLER` and revokes nothing.
2. With the new key, mint replacements for every key the old key minted that your apps still use, and deploy them. Revoking the old key in the next step revokes those keys too, so an app still on one of them stops working.
3. Revoke the old key with the new one. That also revokes every key the old key minted, including any an attacker minted with it. To revoke only the old key and keep the keys it minted, send `?cascade=none`, but then any key an attacker minted keeps working until it's revoked on its own.

Only a write key for every index lists, gets and revokes keys. A write key limited to some indexes gets `403`, and so do search, scoped and issuer keys.

On a tenant served from several regions, keys are created and revoked in its primary region. A Create Key or Revoke Key request that reaches another region answers `409 KEYS_READ_ONLY`, and its message names the primary region. A key created there works in every region within a few seconds.

List Keys also shows keys Captain issued on your tenant, such as a short-lived key support uses to look at your index with you. Those carry `issued_by_captain: true`, and revoking one with your key answers `403 KEY_ISSUED_BY_CAPTAIN`. Ask support to revoke it.

## Sending the key

Every endpoint takes its key the same way, as a bearer token in the `Authorization` header. Each endpoint's reference page names the kind of key it needs.

#### curl

```bash
curl -X POST "https://acme.captain.dev/v1/indexes/products/search" \
  -H "Authorization: Bearer $CAPTAIN_SEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "waterproof hiking boots",
  "k": 3
}'
```

#### Python

```python
import json
import requests

BASE_URL = "https://acme.captain.dev"
API_KEY = "your_search_key"
INDEX = "products"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

response = requests.post(
    f"{BASE_URL}/v1/indexes/{INDEX}/search",
    headers=headers,
    json={
        "query": "waterproof hiking boots",
        "k": 3,
    },
    timeout=30.0,
)

print(json.dumps(response.json(), indent=2))
```

#### TypeScript

```typescript
const BASE_URL = "https://acme.captain.dev";
const API_KEY = "your_search_key";
const index = "products";

const response = await fetch(`${BASE_URL}/v1/indexes/${index}/search`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: "waterproof hiking boots",
    k: 3,
  }),
});

const result = await response.json();
console.log(JSON.stringify(result, null, 2));
```

A missing or unknown key returns `401`. A key without access to the index or operation returns `403`, and so does an index the key cannot see, whether or not that index exists.

## Calling from a browser

A scoped key can be used straight from a web page. A search key works from a browser too, but it reads your analytics, so keep it on your server. Captain answers the browser's preflight for any origin, without a key. A page may send these request headers:

* `Authorization` (or `X-Captain-Key`)
* `Content-Type`
* `X-Captain-Min-Seq`

Answers to a search or scoped key allow the page's origin and let page code read `X-Captain-Seq`, `X-Captain-Applied-Seq` and `X-Request-Id`.

A key can be limited to your own sites. Set that list in `allowed_origins` when you create the key with Create Key, and a request from any other origin then gets `403` `ORIGIN_NOT_ALLOWED`. The origin check stops other websites from using your key in their visitors' browsers. It doesn't stop a server that sends a fake origin. Limit what a leaked key can reach with its scope, and revoke it if it leaks.

Write keys stay on your server. Their answers carry no CORS headers, so a page can't read them.

## Errors

Every error reply has the same shape: a machine code in `error` and a readable `message`. That includes a path no endpoint answers (`404 ROUTE_NOT_FOUND`) and a method an endpoint doesn't take (`405 METHOD_NOT_ALLOWED`).

```json
{"error": "FILTER_INVALID", "message": "filter: unknown field colour", "position": 0, "expected": ["brand", "color", "price"]}
```

| Status | Common codes                                                                                                                                                                                              | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `VALIDATION_ERROR`, `FILTER_INVALID`, `FACET_UNKNOWN`, `PAGE_TOO_DEEP`, `ID_CONFLICT`, `ALGOLIA_KEY_CAN_WRITE`                                                                                            | The request is malformed or names something the index lacks. `ID_CONFLICT`: an Algolia object's own id field differs from its `objectID`. `ALGOLIA_KEY_CAN_WRITE`: the Algolia key sent to Import From Algolia can write to Algolia. Send a key with read permissions only.                                                                                                                                                                               |
| 401    | `UNAUTHORIZED`                                                                                                                                                                                            | The key is missing or unknown.                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 403    | `FORBIDDEN`, `ACCESS_DENIED`, `KEY_ISSUED_BY_CAPTAIN`                                                                                                                                                     | The key may not use this index or operation. `KEY_ISSUED_BY_CAPTAIN`: the key to revoke was issued by Captain's support or console, and only Captain revokes it.                                                                                                                                                                                                                                                                                          |
| 404    | `INDEX_NOT_FOUND`, `RECORD_NOT_FOUND`, `JOB_NOT_FOUND`, `ALIAS_NOT_FOUND`, `ROUTE_NOT_FOUND`                                                                                                              | The index, record, job or alias does not exist, or no endpoint has that path. A `RECORD_NOT_FOUND` reply also carries `legacy_error: "DOCUMENT_NOT_FOUND"`, its former name.                                                                                                                                                                                                                                                                              |
| 405    | `METHOD_NOT_ALLOWED`                                                                                                                                                                                      | The endpoint exists but doesn't take this method.                                                                                                                                                                                                                                                                                                                                                                                                         |
| 409    | `INDEX_EXISTS`, `IMPORT_RUNNING`, `INDEX_NOT_READY`, `IDEMPOTENCY_IN_PROGRESS`, `JOB_NOT_CANCELLABLE`, `INDEX_HAS_ALIAS`, `ALIAS_NOT_INDEX`, `CASCADE_INCLUDES_CALLER`, `KEY_IS_CALLER`, `KEYS_READ_ONLY` | An index or alias with that name already exists, an import into the index is already running, the index has no records yet, a request with the same idempotency key is still running, the job has already started, an alias still points at the index, the name is an alias, not an index, revoking the key would also revoke the key making the call, a key tried to revoke itself, or a key was created or revoked outside the tenant's primary region. |
| 413    | `PAYLOAD_TOO_LARGE`, `INDEX_TOO_LARGE`                                                                                                                                                                    | The request body is too large, or the records would take the index past the tenant's record limit, when one is set.                                                                                                                                                                                                                                                                                                                                       |
| 422    | `IDEMPOTENCY_KEY_REUSED`                                                                                                                                                                                  | An idempotency key was reused with a different request.                                                                                                                                                                                                                                                                                                                                                                                                   |
| 429    | `RATE_LIMITED`                                                                                                                                                                                            | A request with no valid key, from an address sending many of them. Retry after the time in `Retry-After`. Requests with a valid key aren't rate limited: use is fair use.                                                                                                                                                                                                                                                                                 |
| 503    | `UNAVAILABLE`                                                                                                                                                                                             | The service is briefly unavailable. Retry with backoff.                                                                                                                                                                                                                                                                                                                                                                                                   |

A request whose connection closes before any answer arrives never reached Captain. [Connections and retries](/guides/product-search/connections-and-retries) shows when to send it again.

## Limits

| Limit                                | Value                                           |
| ------------------------------------ | ----------------------------------------------- |
| Records per add request              | 1,000 as a JSON array, 100,000 as NDJSON or CSV |
| Records per update or delete request | 1,000                                           |
| Records per get request              | 100                                             |
| Events per request                   | 1,000                                           |
| Searches per multi-search request    | 10                                              |

## Machine-readable guides

Each tenant publishes its own reference:

```
https://acme.captain.dev/v1/openapi.json
https://acme.captain.dev/llms.txt
```

`openapi.json` lists the endpoints in this reference, as deployed on that tenant. `llms.txt` is a short plain-text guide for coding assistants.

## Request ids

Every reply carries an `X-Request-Id` header. Quote it when you contact Captain about a request. A request that sends its own `X-Request-Id` (8 to 128 letters, digits, `.`, `_` or `-`) gets the same id back.

## Next steps

* [Quickstart](/guides/product-search/quickstart)
* [Index Settings](/guides/product-search/settings)
* [Connections and retries](/guides/product-search/connections-and-retries)
* [Product Search API reference](/reference/product-search)