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

# Quickstart

> Create an index, add records, and run your first search, in four calls.

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

This guide goes from nothing to search results. You create an index, add three product records, check that they're live, and search them.

Captain gives you three keys at onboarding, along with your tenant's endpoint: a search key, a write key and an issuer key. The write key creates indexes and adds records, and it stays on your server. The search key runs searches and reads the index's analytics, so keep it on your server too. The issuer key mints short-lived scoped keys, and it stays on your server as well. In a browser or an app, use a scoped key limited to your site with `allowed_origins`. The examples use the tenant `acme`. Replace it with the endpoint from onboarding.

## 1. Create an index

An index holds one kind of record, such as products, articles or listings. Give it a name of lowercase letters, digits, dashes and underscores.

#### curl

```bash
curl -X POST "https://acme.captain.dev/v1/indexes" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "products"
}'
```

#### Python

```python
import json
import requests

BASE_URL = "https://acme.captain.dev"
API_KEY = "your_write_key"

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

response = requests.post(
    f"{BASE_URL}/v1/indexes",
    headers=headers,
    json={
        "name": "products",
    },
    timeout=30.0,
)

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

#### TypeScript

```typescript
const BASE_URL = "https://acme.captain.dev";
const API_KEY = "your_write_key";

const response = await fetch(`${BASE_URL}/v1/indexes`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "products",
  }),
});

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

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

```json Example: (201 Created)
{
  "name": "products",
  "status": "empty",
  "staged_documents": 0,
  "staged_records": 0,
  "record_count": 0,
  "build_id": null,
  "last_job": null,
  "created_at": 1759093140,
  "staged_changes_since_build": false,
  "pending_changes": false,
  "object_search": true,
  "last_build_at": null,
  "serving": null,
  "id_field": "id"
}
```

The new index is `empty` until records arrive. Field types are optional. When you leave them out, Captain types each field from the first records you add. To set them yourself, see [Create Index](/reference/product-search/indexes/create-index) and the field types in [Records](/guides/product-search/records#field-types).

## 2. Add records

Send records as a JSON array. Each record needs an `id` and the fields you want search to know about.

#### curl

```bash
curl -X POST "https://acme.captain.dev/v1/indexes/products/records" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '[
  {
    "id": "sku-10482",
    "title": "Moab 3 Mid Waterproof Hiking Boot",
    "brand": "Merrell",
    "category": "Hiking Boots",
    "price": 139.0,
    "rating": 4.6,
    "image_url": "https://cdn.acme.example/img/sku-10482.jpg",
    "description": "Waterproof leather and mesh mid boot with a Vibram TC5+ outsole and a breathable membrane."
  },
  {
    "id": "sku-10517",
    "title": "X Ultra 4 Mid GTX Hiking Boot",
    "brand": "Salomon",
    "category": "Hiking Boots",
    "price": 165.0,
    "rating": 4.5,
    "image_url": "https://cdn.acme.example/img/sku-10517.jpg",
    "description": "GORE-TEX mid boot with a Contagrip outsole, built for rocky trails."
  },
  {
    "id": "sku-11020",
    "title": "Targhee III Waterproof Mid Boot",
    "brand": "KEEN",
    "category": "Hiking Boots",
    "price": 150.0,
    "rating": 4.4,
    "image_url": "https://cdn.acme.example/img/sku-11020.jpg",
    "description": "Waterproof nubuck boot with a KEEN.DRY membrane and a wide toe box."
  }
]'
```

#### Python

```python
import json
import requests
import uuid

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

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
    "Idempotency-Key": str(uuid.uuid4()),
}

response = requests.post(
    f"{BASE_URL}/v1/indexes/{INDEX}/records",
    headers=headers,
    json=[
        {
            "id": "sku-10482",
            "title": "Moab 3 Mid Waterproof Hiking Boot",
            "brand": "Merrell",
            "category": "Hiking Boots",
            "price": 139.0,
            "rating": 4.6,
            "image_url": "https://cdn.acme.example/img/sku-10482.jpg",
            "description": "Waterproof leather and mesh mid boot with a Vibram TC5+ outsole and a breathable membrane.",
        },
        {
            "id": "sku-10517",
            "title": "X Ultra 4 Mid GTX Hiking Boot",
            "brand": "Salomon",
            "category": "Hiking Boots",
            "price": 165.0,
            "rating": 4.5,
            "image_url": "https://cdn.acme.example/img/sku-10517.jpg",
            "description": "GORE-TEX mid boot with a Contagrip outsole, built for rocky trails.",
        },
        {
            "id": "sku-11020",
            "title": "Targhee III Waterproof Mid Boot",
            "brand": "KEEN",
            "category": "Hiking Boots",
            "price": 150.0,
            "rating": 4.4,
            "image_url": "https://cdn.acme.example/img/sku-11020.jpg",
            "description": "Waterproof nubuck boot with a KEEN.DRY membrane and a wide toe box.",
        },
    ],
    timeout=30.0,
)

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

#### TypeScript

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

const response = await fetch(`${BASE_URL}/v1/indexes/${index}/records`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify([
    {
      id: "sku-10482",
      title: "Moab 3 Mid Waterproof Hiking Boot",
      brand: "Merrell",
      category: "Hiking Boots",
      price: 139.0,
      rating: 4.6,
      image_url: "https://cdn.acme.example/img/sku-10482.jpg",
      description: "Waterproof leather and mesh mid boot with a Vibram TC5+ outsole and a breathable membrane.",
    },
    {
      id: "sku-10517",
      title: "X Ultra 4 Mid GTX Hiking Boot",
      brand: "Salomon",
      category: "Hiking Boots",
      price: 165.0,
      rating: 4.5,
      image_url: "https://cdn.acme.example/img/sku-10517.jpg",
      description: "GORE-TEX mid boot with a Contagrip outsole, built for rocky trails.",
    },
    {
      id: "sku-11020",
      title: "Targhee III Waterproof Mid Boot",
      brand: "KEEN",
      category: "Hiking Boots",
      price: 150.0,
      rating: 4.4,
      image_url: "https://cdn.acme.example/img/sku-11020.jpg",
      description: "Waterproof nubuck boot with a KEEN.DRY membrane and a wide toe box.",
    },
  ]),
});

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

**`Example: (202 Accepted)`**

```json Example: (202 Accepted)
{
  "index": "products",
  "accepted": 3,
  "new": 3,
  "replaced": 0,
  "rejected": [],
  "rejected_count": 0,
  "duplicates": 0,
  "warnings": [],
  "record_count": 3,
  "max_records": null,
  "seq": 1,
  "accepted_at": 1759093201.552,
  "regions_applied": [
    "us-east-1"
  ],
  "regions_pending": []
}
```

The reply confirms that three new records were accepted, and gives the change a `seq` number. Records become searchable within a few seconds. There's no separate build step.

## 3. Check the records are live

Get Update Status reports the last change search has applied. When `applied_seq` reaches the `seq` from step 2, the records are live.

#### curl

```bash
curl -X GET "https://acme.captain.dev/v1/indexes/products/updates" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY"
```

#### Python

```python
import json
import requests

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

headers = {
    "Authorization": f"Bearer {API_KEY}",
}

response = requests.get(
    f"{BASE_URL}/v1/indexes/{INDEX}/updates",
    headers=headers,
    timeout=30.0,
)

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

#### TypeScript

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

const response = await fetch(`${BASE_URL}/v1/indexes/${index}/updates`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
  },
});

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

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "accepted_seq": 1,
  "applied_seq": 1,
  "pending": 0
}
```

In an app, you rarely need this check. It's useful in a script that adds records and then searches them straight away.

## 4. Run a search

Searches use the search key. Send the words people type as `query`.

#### 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));
```

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "hits": [
    {
      "id": "sku-10482",
      "title": "Moab 3 Mid Waterproof Hiking Boot",
      "brand": "Merrell",
      "category": "Hiking Boots",
      "price": 139.0,
      "rating": 4.6,
      "image_url": "https://cdn.acme.example/img/sku-10482.jpg",
      "description": "Waterproof leather and mesh mid boot with a Vibram TC5+ outsole and a breathable membrane.",
      "score": 0.912
    },
    {
      "id": "sku-11020",
      "title": "Targhee III Waterproof Mid Boot",
      "brand": "KEEN",
      "category": "Hiking Boots",
      "price": 150.0,
      "rating": 4.4,
      "image_url": "https://cdn.acme.example/img/sku-11020.jpg",
      "description": "Waterproof nubuck boot with a KEEN.DRY membrane and a wide toe box.",
      "score": 0.874
    },
    {
      "id": "sku-10517",
      "title": "X Ultra 4 Mid GTX Hiking Boot",
      "brand": "Salomon",
      "category": "Hiking Boots",
      "price": 165.0,
      "rating": 4.5,
      "image_url": "https://cdn.acme.example/img/sku-10517.jpg",
      "description": "GORE-TEX mid boot with a Contagrip outsole, built for rocky trails.",
      "score": 0.851
    }
  ],
  "total": 3,
  "query_id": "7f3a9c2e4b1d48e6a0c5d2f18b93e470",
  "query_understanding": {
    "category_match": [
      "Hiking Boots"
    ],
    "evidence": 3,
    "evidence_missing": 0,
    "missing_words": []
  },
  "request": {
    "mode": "full",
    "build": "b20260928201247"
  },
  "timing": {
    "engine_ms": 21.4
  }
}
```

The reply lists results under `hits`, best first. Each hit carries the record's `id`, its fields and a relevance `score`. `total` is an estimate of how many records matched, not an exact count, so page with `page.has_more` rather than `total`. Send `"exact_total": true` when you need a count to show, and check `total_exact` in the reply. `timing.engine_ms` is how long Captain took to answer, in milliseconds.

## 5. Add a filter

A filter keeps only the records that meet every condition. [List Filters](/reference/product-search/indexes/list-filters) shows which fields you can filter on and which operators each one accepts.

#### 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": "hiking boots",
  "filter": "brand IN [\"Merrell\", \"Salomon\"] AND price <= 150",
  "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": "hiking boots",
        "filter": 'brand IN ["Merrell", "Salomon"] AND price <= 150',
        "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: "hiking boots",
    filter: 'brand IN ["Merrell", "Salomon"] AND price <= 150',
    k: 3,
  }),
});

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

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "hits": [
    {
      "id": "sku-10482",
      "title": "Moab 3 Mid Waterproof Hiking Boot",
      "brand": "Merrell",
      "category": "Hiking Boots",
      "price": 139.0,
      "rating": 4.6,
      "image_url": "https://cdn.acme.example/img/sku-10482.jpg",
      "description": "Waterproof leather and mesh mid boot with a Vibram TC5+ outsole and a breathable membrane.",
      "score": 0.904
    }
  ],
  "total": 1,
  "query_id": "2c81d7e05a3f4b9c8e16f0a4d72b5c93",
  "query_understanding": {
    "category_match": [
      "Hiking Boots"
    ],
    "evidence": 2,
    "evidence_missing": 0,
    "missing_words": []
  },
  "request": {
    "mode": "full",
    "build": "b20260928201247",
    "filter": "brand IN [\"Merrell\", \"Salomon\"] AND price <= 150"
  },
  "timing": {
    "engine_ms": 21.4
  }
}
```

Only the Merrell boot is both from one of the two brands and at or under \$150, so it's the one result.

## 6. Count facets

Facets count how many results have each value of a field, which is what a sidebar of brand or category checkboxes 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": "hiking boots",
  "k": 3,
  "facet_keys": [
    "brand"
  ]
}'
```

#### 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": "hiking boots",
        "k": 3,
        "facet_keys": ["brand"],
    },
    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: "hiking boots",
    k: 3,
    facet_keys: ["brand"],
  }),
});

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

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "hits": [
    {
      "id": "sku-10482",
      "title": "Moab 3 Mid Waterproof Hiking Boot",
      "brand": "Merrell",
      "category": "Hiking Boots",
      "price": 139.0,
      "rating": 4.6,
      "image_url": "https://cdn.acme.example/img/sku-10482.jpg",
      "description": "Waterproof leather and mesh mid boot with a Vibram TC5+ outsole and a breathable membrane.",
      "score": 0.921
    },
    {
      "id": "sku-10517",
      "title": "X Ultra 4 Mid GTX Hiking Boot",
      "brand": "Salomon",
      "category": "Hiking Boots",
      "price": 165.0,
      "rating": 4.5,
      "image_url": "https://cdn.acme.example/img/sku-10517.jpg",
      "description": "GORE-TEX mid boot with a Contagrip outsole, built for rocky trails.",
      "score": 0.883
    },
    {
      "id": "sku-11020",
      "title": "Targhee III Waterproof Mid Boot",
      "brand": "KEEN",
      "category": "Hiking Boots",
      "price": 150.0,
      "rating": 4.4,
      "image_url": "https://cdn.acme.example/img/sku-11020.jpg",
      "description": "Waterproof nubuck boot with a KEEN.DRY membrane and a wide toe box.",
      "score": 0.862
    }
  ],
  "total": 3,
  "facets": {
    "brand": [
      {
        "value": "KEEN",
        "count": 1
      },
      {
        "value": "Merrell",
        "count": 1
      },
      {
        "value": "Salomon",
        "count": 1
      }
    ]
  },
  "facets_exact": true,
  "facet_set_size": 3,
  "query_id": "b6e2a9f14c7d4e038a5f91c2d6b7e814",
  "request": {
    "mode": "full",
    "build": "b20260928201247"
  },
  "timing": {
    "engine_ms": 21.4
  }
}
```

`facets` in the reply holds the count for each value of each requested field.

## Next steps

* [Records](/guides/product-search/records): structure records, update and delete them, and see how changes reach search.
* [Search](/guides/product-search/search): typeahead, browse, similar records and multiple searches.
* [Analytics](/guides/product-search/analytics): send click and conversion events, and read what people search for.