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

# Analytics

> Send click, conversion and view events, and read search analytics for an index.

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

## Send events

Send events from the application with the search key, or from a browser with a scoped key. Tie a click or conversion to the search that produced it with the `query_id` from the search reply.

Send `user_token` on each search, the same value as the events' `userToken`. A search with a user token is a tracked search: the click-through and conversion rates count only tracked searches, and Count Users counts distinct user tokens. A search without one still counts as a search. On the Algolia-compatible routes, `userToken` or the `X-Algolia-UserToken` header does the same.

#### curl

```bash
curl -X POST "https://acme.captain.dev/v1/events" \
  -H "Authorization: Bearer $CAPTAIN_SEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    {
      "eventType": "click",
      "eventName": "Result clicked",
      "index": "products",
      "userToken": "user-481",
      "queryID": "7f3a9c2e4b1d48e6a0c5d2f18b93e470",
      "objectIDs": [
        "sku-10482"
      ],
      "positions": [
        1
      ]
    }
  ]
}'
```

#### Python

```python
import json
import requests

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

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

response = requests.post(
    f"{BASE_URL}/v1/events",
    headers=headers,
    json={
        "events": [
            {
                "eventType": "click",
                "eventName": "Result clicked",
                "index": "products",
                "userToken": "user-481",
                "queryID": "7f3a9c2e4b1d48e6a0c5d2f18b93e470",
                "objectIDs": ["sku-10482"],
                "positions": [1],
            },
        ],
    },
    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 response = await fetch(`${BASE_URL}/v1/events`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    events: [
      {
        eventType: "click",
        eventName: "Result clicked",
        index: "products",
        userToken: "user-481",
        queryID: "7f3a9c2e4b1d48e6a0c5d2f18b93e470",
        objectIDs: ["sku-10482"],
        positions: [1],
      },
    ],
  }),
});

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

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

```json Example: (200 OK)
{
  "accepted": 1
}
```

Up to 1,000 events per request. A click tied to a search carries one position per record id, starting at 1.

## Read analytics

#### curl

```bash
curl -G "https://acme.captain.dev/v1/indexes/products/analytics" \
  -H "Authorization: Bearer $CAPTAIN_SEARCH_KEY" \
  --data-urlencode "days=7"
```

#### 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}",
}

response = requests.get(
    f"{BASE_URL}/v1/indexes/{INDEX}/analytics",
    headers=headers,
    params={"days": 7},
    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 params = new URLSearchParams({ days: "7" });

const response = await fetch(`${BASE_URL}/v1/indexes/${index}/analytics?${params}`, {
  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)
{
  "dataset": "products",
  "days": 7,
  "search_requests": 48213,
  "searches": 9604,
  "top_queries": [
    {
      "query": "hiking boots",
      "count": 412,
      "avg_hits": 42.0
    },
    {
      "query": "rain jacket",
      "count": 355,
      "avg_hits": 18.0
    },
    {
      "query": "trail running shoes",
      "count": 298,
      "avg_hits": 37.0
    }
  ],
  "zero_result_queries": [
    {
      "query": "gaiter size chart",
      "count": 21
    },
    {
      "query": "crampons",
      "count": 14
    }
  ],
  "zero_result_rate": 0.0187,
  "events": {
    "click": 3920,
    "conversion": 611,
    "view": 12078
  },
  "click_through_rate": 0.3842,
  "mean_click_position": 2.34,
  "conversion_rate": 0.0642,
  "tracked_searches": 9517
}
```

| Field                                       | Meaning                                                               |
| ------------------------------------------- | --------------------------------------------------------------------- |
| `searches`                                  | Searches in the period. Keystrokes on the way to a search count once. |
| `top_queries`                               | The most frequent searches, with their average number of results.     |
| `zero_result_queries`, `zero_result_rate`   | Searches that returned nothing.                                       |
| `click_through_rate`, `mean_click_position` | Share of tracked searches with a click, and where the clicks landed.  |
| `conversion_rate`                           | Share of tracked searches followed by a conversion.                   |
| `events`                                    | Event counts by type.                                                 |
| `tracked_searches`                          | Searches that carried a user token, the base for the rates above.     |

Before an index's first build, analytics return `200` with empty reports, since the index hasn't served any searches yet.

Query logs can hold sensitive text. On an index with per-record access control, Captain provides analytics until a dedicated analytics key is available.

## Analytics by metric

Each metric also has its own endpoint, for dashboards that chart one number over time. They take `start_date` and `end_date` (UTC, inclusive, `YYYY-MM-DD`, the last 7 days by default). For an exact window, send `start_time` and `end_time` instead, as unix seconds or ISO 8601 in UTC (`2026-10-10T10:00:00Z`). `end_time` is exclusive. The list endpoints also take `limit` and `offset`. Every analytics answer carries `partial`. Analytics requests usually answer within 6 seconds and within about 7.5 seconds at most, even when several dashboards ask at once. A window too large to read whole in that time (more than 500,000 searches, or more than about 3.5 seconds of reading) answers with what was counted, `partial: true`, `partial_reason` (`row_budget` or `time_budget`) and `counted_through`, the time of the last record counted. Ask for fewer days to get whole numbers. The same report asked again within a minute, with no new searches since, answers at once.

| Endpoint                                                                               | Returns                                                                                                                                      |
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| [Count Searches](/reference/product-search/analytics/count-searches)                   | Searches in total and per day.                                                                                                               |
| [List Top Searches](/reference/product-search/analytics/list-top-searches)             | The most frequent searches, with average results, and click-through and conversion rates with `with_rates=true`.                             |
| [List No-Result Searches](/reference/product-search/analytics/list-no-result-searches) | The most frequent searches that returned nothing, and the no-result rate.                                                                    |
| [List No-Click Searches](/reference/product-search/analytics/list-no-click-searches)   | The most frequent tracked searches with no click, and the no-click rate.                                                                     |
| [Get Clicks](/reference/product-search/analytics/get-clicks)                           | The click-through rate, the average click position and clicks per position.                                                                  |
| [Get Conversions](/reference/product-search/analytics/get-conversions)                 | The share of tracked searches followed by a conversion.                                                                                      |
| [List Top Hits](/reference/product-search/analytics/list-top-hits)                     | The records clicked most often, for all searches, for one (`search`), or for up to 25 at once (`searches`, separated by commas).             |
| [Count Users](/reference/product-search/analytics/count-users)                         | Distinct user tokens, in total and per day.                                                                                                  |
| [List Top Filters](/reference/product-search/analytics/list-top-filters)               | The fields searches filtered on most often.                                                                                                  |
| [List Filter Values](/reference/product-search/analytics/list-filter-values)           | The values one field was filtered by most often.                                                                                             |
| [Get Revenue](/reference/product-search/analytics/get-revenue)                         | Revenue, orders and average order value that searches earned, by day, from purchase events, with the shopper funnel from search to purchase. |

Rates come with a daily series, so one call fills a chart.

For example, the top searches of the last week:

#### curl

```bash
curl -G "https://acme.captain.dev/v1/indexes/products/analytics/searches" \
  -H "Authorization: Bearer $CAPTAIN_SEARCH_KEY" \
  --data-urlencode "start_date=2026-09-22" \
  --data-urlencode "end_date=2026-09-28" \
  --data-urlencode "limit=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}",
}

response = requests.get(
    f"{BASE_URL}/v1/indexes/{INDEX}/analytics/searches",
    headers=headers,
    params={"start_date": "2026-09-22", "end_date": "2026-09-28", "limit": 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 params = new URLSearchParams({ start_date: "2026-09-22", end_date: "2026-09-28", limit: "3" });

const response = await fetch(`${BASE_URL}/v1/indexes/${index}/analytics/searches?${params}`, {
  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)
{
  "start_date": "2026-09-22",
  "end_date": "2026-09-28",
  "searches": [
    {
      "search": "hiking boots",
      "count": 412,
      "avg_hits": 42.0
    },
    {
      "search": "rain jacket",
      "count": 355,
      "avg_hits": 18.0
    },
    {
      "search": "trail running shoes",
      "count": 298,
      "avg_hits": 37.0
    }
  ]
}
```