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

# Browse Records

POST https://acme.captain.dev/v1/indexes/{index}/browse
Content-Type: application/json

List records with filters, sort, pages and facets and no query text, for category and listing pages.

Reference: https://docs.captain.dev/reference/product-search/searches/browse-records

## Authentication

- `Authorization` header (bearer token, required) — A search key, issued by Captain. Sent as Authorization: Bearer \<key>.
- `Authorization` header (bearer token, required) — A scoped key for one end user, minted from an issuer key that Captain issues. Sent as Authorization: Bearer \<key>.

## Request

### Path parameters

- `index` (string, required) — index name

### Body (application/json)

This endpoint expects a BrowseRequest.

- `filters` (map from string to any, optional) — field -> value (exact), or list (any of). Also price\_min, price\_max, rating\_min, t\_\<field>*min / t*\<field>\_max for typed numbers (flexible schema), attributes.\<key> for attribute values. A filterable field is named as the settings name it: a value or a list of values, or, on a number or date field, a range \{gte, gt, lte, lt}. A bare date as lte covers that whole day (lte 2003-12-31 keeps filings made on the 31st). The filter expression (filter) says the same and more
- `k` (integer, optional, default: 24)
- `rule_contexts` (list of string, optional) — rule contexts (Algolia ruleContexts): a rule whose condition has a context fires only when the search sends it
- `page` (integer, optional, default: 0)
- `facet_keys` (list of string, optional) — the facets to count, by field name (as the answer names them). A date facet, a facet with more than 1,000 distinct values at build, or a field that is not a facet with more than 500, has no value counts and is refused (400); capabilities.fields.facet_counts lists the facets that count
- `facet_limit` (integer, optional)
- `filter` (string, optional)
- `sort` (list of BrowseRequestSortItems, optional) — exact over every match
- `analytics` (boolean, optional, default: true) — false: not logged for analytics, as in search
- `fields` (BrowseRequestFields, optional)
- `facet_query` (BrowseRequestFacetQuery, optional) — as in search
- `facet_stats` (boolean, optional) — as in search

## Response

### 200

results

- `hits` (list of Hit, optional)
- `total` (integer, optional) — search: the number of candidates the search ranked (an estimate, not a count of every match) unless the search asked exact_total, when it is the number the Algolia host answers as nbHits (see total_basis). Page with page.has_more, not total. Browse: every record the filter keeps. With group_by too: records, not groups
- `total_exact` (boolean, optional) — true when total is an exact number (search with exact_total: total_basis pageable or all_matches). False: an estimate (candidates) or the 1,000 floor (paging_limit)
- `total_basis` (enum, optional) — what total is: pageable (exact: the hits the search can page through, under 1,000), all_matches (exact: every record that contains every query word, 1,000 or more), paging_limit (1,000, a floor: more may match), candidates (exact_total not asked, or type-ahead), or why an asked-for exact count fell back: busy, time_budget, error, no_required_words (no query word is required, so every record would match)
  - Allowed values: `pageable`, `all_matches`, `paging_limit`, `candidates`, `busy`, `time_budget`, `error`, `no_required_words`
- `facet_detail` (map from string to SearchResponseFacetDetail, optional) — per facet in facets: basis all_matches (counted over every match, exact true) or relevant_results (counted over the relevance-cut set, exact false, with the reason when an asked-for exact count fell back)
- `grouped` (SearchResponseGrouped, optional) — present when results are grouped: the ranking, collapsed by the field's value. A grouped search reads the 100 most relevant records, and further down the top 1,000 when k x size x 4 needs more; depth says how many it read
- `facets` (map from string to list of SearchResponseFacetsItems, optional)
- `facets_exact` (boolean, optional) — true only when every facet was counted over every matching record (browse). Search counts facets over the relevance-cut set (the most relevant results, up to 2,000): false
- `facet_set_size` (integer, optional)
- `query_id` (string, optional)
- `assignment` (AssignmentDecision, optional) — only on a search of a catalog with an assignment that sent a user_token: the shopper's group, and the index that served it
- `analytics` (boolean, optional) — false when the request sent analytics: false (not logged, no query_id); absent otherwise
- `query_understanding` (QueryUnderstanding, optional) — what the engine understood. Keys appear only when they apply; keys not listed here are diagnostics and may change
- `user_data` (list of SearchResponseUserDataItems, optional) — the userData of each rule applied to this search, in rule order (at most 10). Absent when no applied rule has any
- `facet_ordering` (FacetOrdering, optional) — how a page orders facets and their values (Algolia renderingContent.facetOrdering, snake_case). Returned on search and browse when the index or an applied rule sets one; the client orders with it
- `redirect` (SearchResponseRedirect, optional) — an applied rule's redirect (the first in rule order): the page to send the shopper to instead of showing these results. Absent when no applied rule redirects
- `constraints_removed` (integer, optional) — constraints: strict: hits removed because a parsed quantity failed. hits [] with this > 0 means nothing meets the query's quantities
- `sort` (SearchResponseSort, optional) — search with sort: which field and over what set
- `page` (SearchResponsePage, optional) — search with page or per_page
- `facet_query` (SearchResponseFacetQuery, optional)
- `facet_stats` (map from string to SearchResponseFacetStats, optional)
- `request` (SearchResponseRequest, optional) — What ran. mode is full for Search Records and instant for Search As You Type, which skips the slower ranking steps to answer each keystroke. build is the index build that answered.
- `timing` (SearchResponseTiming, optional)

## Types

### BrowseRequestSortItems

- `field` (string, optional)
- `order` (enum, optional)
  - Allowed values: `asc`, `desc`
- `near` (BrowseRequestSortItemsNear, optional) — on a geo field: the point to measure from. asc puts the nearest records first

### BrowseRequestFields

### BrowseRequestFacetQuery

as in search

### Hit

a hit is its record: the id, every field of the record as it was sent (url and text fields included; access lists never), and score. title, brand, category, price, rating and image_url appear when the record has a value for them (a product card's fields, under those names even when the record names them otherwise); description is the record's own description field. fields, _explain, _highlight, _citations, group and identifier_match come when asked for. An index last built by an older engine answers in the product shape until its next build

- `group` (HitGroup, optional) — group_by: this hit is its group's best-ranked record; hits are up to size - 1 more members, in rank order (value null: the record has none, a group of its own)
- `id` (string, optional)
- `title` (string, optional, nullable)
- `brand` (string, optional, nullable)
- `category` (string, optional, nullable)
- `price` (double, optional, nullable)
- `rating` (double, optional, nullable)
- `image_url` (string, optional, nullable)
- `description` (string, optional)
- `score` (double, optional) — the relevance score, from keyword and meaning-based matching together. Optional: on an access-controlled index, results for keys other than admin come without scores, because scores depend on the whole catalog, including records the caller cannot see. The order of hits is the ranking either way
- `fields` (HitFields, optional) — with fields: typed values
- `identifier_match` (HitIdentifierMatch, optional) — this hit's identifier field holds, whole, a code the query typed ("matched cusip 66537J812"). Absent when there is no such match
- `_explain` (HitExplain, optional)
- `_highlight` (map from string to string, optional)
- `_citations` (list of HitCitationsItems, optional)
- `geo_distance_m` (double, optional) — with a geo sort: meters from the sort's near point. Absent when the record has no location

### SearchResponseFacetDetail

- `basis` (enum, optional)
  - Allowed values: `all_matches`, `relevant_results`
- `exact` (boolean, optional)
- `reason` (string, optional)

### SearchResponseGrouped

present when results are grouped: the ranking, collapsed by the field's value. A grouped search reads the 100 most relevant records, and further down the top 1,000 when k x size x 4 needs more; depth says how many it read

- `field` (string, optional)
- `size` (integer, optional)
- `depth` (integer, optional) — ranked candidates collapsed
- `complete` (boolean, optional) — false: more records matched than the depth, so a group (or a member) ranked below it is not shown
- `groups` (integer, optional) — groups among all matches. Present only when complete is true; absent when complete is false, because the count is then unknown

### SearchResponseFacetsItems

- `value` (any, optional)
- `count` (integer, optional)

### AssignmentDecision

only on a search of a catalog with an assignment that sent a user_token: the shopper's group, and the index that served it

- `layer` (string, required)
- `group` (string, required)
- `index` (string, required) — the copy that served the search, or the catalog itself
- `fallback` (boolean, required) — true: the group's copy could not answer here (missing, not built, loading), so the catalog served

### QueryUnderstanding

what the engine understood. Keys appear only when they apply; keys not listed here are diagnostics and may change

- `corrections` (list of list of string, optional) — [[typed, fixed]]
- `splitjoin` (list of list of string, optional) — [[typed, fixed]] word splits and joins
- `as_typed` (boolean, optional) — the request asked for as_typed: nothing above rewrote the query
- `reduced` (boolean, optional) — present (true) only when this answer ran with reduced features: spelling fixes, completions, suggestions and zero-result answers may be off, so treat a missing correction as not checked. Results are still filtered to the caller as always. It names no cause. Absent otherwise, never false
- `price_filter` (QueryUnderstandingPriceFilter, optional) — a price phrase in the query, applied as a filter
- `constraints` (list of list of any, optional) — [[quantity, value in the quantity's base unit, op]], e.g. ["weight", 6.61, "le"]
- `completions` (list of string, optional) — typeahead
- `partial` (string, optional) — typeahead: the word still being typed
- `identifiers` (list of string, optional) — query tokens treated as codes
- `identifier_tier` (integer, optional) — how many leading hits matched an identifier exactly
- `held_top` (boolean, optional)
- `evidence` (integer, optional)
- `evidence_missing` (integer, optional)
- `missing_words` (list of string, optional) — typed words no record carries
- `no_results` (string, optional) — present only when zero hits is the intended answer: the reason
- `relaxed` (QueryUnderstandingRelaxed, optional) — answered without some typed words: 'No match for \<dropped>. Showing results for \<query>' (empty query = related records). Filters are never relaxed
- `relaxed_reason` (string, optional)
- `category_match` (list of string, optional)
- `synonyms` (list of string, optional)
- `rules` (list of string, optional)

### SearchResponseUserDataItems

### FacetOrdering

how a page orders facets and their values (Algolia renderingContent.facetOrdering, snake_case). Returned on search and browse when the index or an applied rule sets one; the client orders with it

- `facets` (FacetOrderingFacets, optional)
- `values` (map from string to FacetOrderingValues, optional)

### SearchResponseRedirect

an applied rule's redirect (the first in rule order): the page to send the shopper to instead of showing these results. Absent when no applied rule redirects

- `url` (string, optional)
- `rule` (string, optional) — the rule's objectID

### SearchResponseSort

search with sort: which field and over what set

### SearchResponsePage

search with page or per_page

- `number` (integer, optional)
- `per_page` (integer, optional)
- `has_more` (boolean, optional) — another page exists within the depth
- `depth` (integer, optional) — how deep search pages reach (1,000 relevant records)

### SearchResponseFacetQuery

- `field` (string, optional)
- `prefix` (string, optional)
- `values` (list of SearchResponseFacetQueryValuesItems, optional)
- `exhaustive` (boolean, optional)

### SearchResponseFacetStats

### SearchResponseRequest

What ran. mode is full for Search Records and instant for Search As You Type, which skips the slower ranking steps to answer each keystroke. build is the index build that answered.

### SearchResponseTiming

- `engine_ms` (double, optional)
- `stages` (SearchResponseTimingStages, optional)

### BrowseRequestSortItemsNear

on a geo field: the point to measure from. asc puts the nearest records first

- `lat` (double, required)
- `lng` (double, required)

### HitGroup

group_by: this hit is its group's best-ranked record; hits are up to size - 1 more members, in rank order (value null: the record has none, a group of its own)

- `field` (string, optional)
- `value` (any, optional)
- `hits` (list of HitGroupHitsItems, optional)

### HitFields

with fields: typed values

### HitIdentifierMatch

this hit's identifier field holds, whole, a code the query typed ("matched cusip 66537J812"). Absent when there is no such match

- `field` (string, optional)
- `value` (string, optional)

### HitExplain

- `lanes` (map from string to integer, optional)
- `fused_score` (double, optional) — optional: withheld with score
- `final_score` (double, optional) — optional: withheld with score
- `identifier_match` (boolean, optional)
- `matched` (map from string to list of string, optional)
- `preferred` (double, optional)

### HitCitationsItems

- `field` (string, optional)
- `child` (HitCitationsItemsChild, optional)
- `start` (integer, optional)
- `end` (integer, optional)
- `text` (string, optional)
- `highlighted` (string, optional)
- `matched` (list of string, optional)
- `score` (double, optional)

### QueryUnderstandingPriceFilter

a price phrase in the query, applied as a filter

- `price_min` (double, optional)
- `price_max` (double, optional)

### QueryUnderstandingRelaxed

answered without some typed words: 'No match for \<dropped>. Showing results for \<query>' (empty query = related records). Filters are never relaxed

- `dropped` (list of string, optional)
- `query` (string, optional)

### FacetOrderingFacets

- `order` (list of string, optional)

### FacetOrderingValues

- `order` (list of string, optional) — values pinned first, in this order
- `sort_remaining_by` (enum, optional, default: count)
  - Allowed values: `count`, `alpha`, `hidden`
- `hide` (list of string, optional)

### SearchResponseFacetQueryValuesItems

### SearchResponseTimingStages

### HitGroupHitsItems

### HitCitationsItemsChild

- `index` (integer, optional)
- `id` (string, optional)
- `title` (string, optional)

## Examples

**Request**

```json
{
  "k": 3,
  "facet_keys": [
    "brand"
  ],
  "filter": "category = \"Hiking Boots\"",
  "sort": [
    {
      "field": "price",
      "order": "asc"
    }
  ]
}
```

**Response**

```json
{
  "hits": [
    {
      "id": "sku-10482",
      "title": "Moab 3 Mid Waterproof Hiking Boot",
      "brand": "Merrell",
      "category": "Hiking Boots",
      "price": 139,
      "rating": 4.6,
      "image_url": "https://cdn.acme.example/img/sku-10482.jpg"
    },
    {
      "id": "sku-11020",
      "title": "Targhee III Waterproof Mid Boot",
      "brand": "KEEN",
      "category": "Hiking Boots",
      "price": 150,
      "rating": 4.4,
      "image_url": "https://cdn.acme.example/img/sku-11020.jpg"
    },
    {
      "id": "sku-10517",
      "title": "X Ultra 4 Mid GTX Hiking Boot",
      "brand": "Salomon",
      "category": "Hiking Boots",
      "price": 165,
      "rating": 4.5,
      "image_url": "https://cdn.acme.example/img/sku-10517.jpg"
    }
  ],
  "total": 42,
  "facets": {
    "brand": [
      {
        "value": "Merrell",
        "count": 14
      },
      {
        "value": "Salomon",
        "count": 11
      },
      {
        "value": "KEEN",
        "count": 9
      },
      {
        "value": "Columbia",
        "count": 8
      }
    ]
  },
  "facets_exact": true,
  "timing": {
    "engine_ms": 6.2
  }
}
```

**SDK Code**

```python Python REST
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}/browse",
    headers=headers,
    json={
        "filter": 'category = "Hiking Boots"',
        "sort": [{"field": "price", "order": "asc"}],
        "k": 3,
        "facet_keys": ["brand"],
    },
    timeout=30.0,
)

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

```typescript TypeScript REST
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}/browse`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filter: 'category = "Hiking Boots"',
    sort: [{ field: "price", order: "asc" }],
    k: 3,
    facet_keys: ["brand"],
  }),
});

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