## Cursor pagination

[`/v1/catalog`](/reference) is the one paginated endpoint here — the
complete filtered set. It uses opaque cursors, not page numbers or offsets:

| Parameter | Default | Max | Notes |
|---|---|---|---|
| `limit` | 100 | 500 | Rows per page |
| `cursor` | none | — | Opaque — from a previous response's `pagination.next_cursor` |

Take `next_cursor` exactly as returned and send it back exactly as received.
Never construct one by hand, decode it, or edit it — its internal shape is
not part of the contract and can change without notice.

## `has_more`

`pagination.has_more` tells you whether another page exists. When it's
`false`, `next_cursor` is `null` and you're done.

## Stable ordering

Rows are ordered by [`sku`](/docs/data-dictionary) within the filters you
requested, and that ordering doesn't shift under you mid-walk — a page can't be reshuffled by
data arriving between your requests.

## Filters

The same six filters apply identically on `/v1/sample`, `/v1/catalog`, and
`/v1/coverage` — all optional:

| Parameter | Format |
|---|---|
| `retailer` | retailer slug |
| `category` | category slug |
| `department` | department slug |
| `rcd` | one or more `retailer:category:department` triples, comma-separated — shorthand for the three above |
| `start_date` | ISO 8601 (`YYYY-MM-DD`) |
| `end_date` | ISO 8601 (`YYYY-MM-DD`) |

`start_date`/`end_date` bound `crawl_date`, not the retailer's own change
date — see [The Product object](/docs/objects/product). A wrong `retailer`,
`category`, or `department` value fails with `valid_options` naming the real
ones — see [Error handling](/docs/error-handling).

## Invalid cursor

A cursor that's expired, malformed, or wasn't issued by this API returns
[`invalid_cursor`](/errors#invalid_cursor) (400, not
[retryable](/docs/error-handling)). There's exactly one recovery: restart
the walk from page one, without a `cursor` parameter. Don't try to repair a
bad cursor — it isn't decodable in a way that's meaningful to you.

## A two-request walk

```bash
curl "https://api.stylumia.com/v1/catalog?retailer=home_depot&category=power_tools&limit=2" \
  -H "Authorization: Bearer $STYLUMIA_KEY"
  # Never commit your key — see /docs/key-security
```

```json
{
  "data": [
    { "product_id": "prd_5741354fa5d111f0a1ce7bcc", "sku": "1003097550", "retailer": "home_depot", "category": "power_tools", "...": "..." },
    { "product_id": "prd_9a3fddc7f5ec69b5913b5614", "sku": "1003097557", "retailer": "home_depot", "category": "power_tools", "...": "..." }
  ],
  "pagination": { "limit": 2, "total_matching": 6, "next_cursor": "eyJvIjogMn0=", "has_more": true },
  "meta": {
    "request_id": "req_1a2b3c9f7e0d452186af",
    "resolved": { "retailer": "home_depot", "category": "power_tools", "department": null, "rcd": null, "start_date": null, "end_date": null },
    "coverage": { "combinations": 1, "matching_rows": 6, "corpus_rows": 2361, "earliest_date": "2026-06-14", "latest_date": "2026-08-14" }
  }
}
```

`pagination.limit` and `pagination.total_matching` only appear on
`/v1/catalog` — `total_matching` is the size of the whole filtered set, not
just this page. Take `next_cursor` and send it back — with the same filters,
not just the cursor:

```bash
curl "https://api.stylumia.com/v1/catalog?retailer=home_depot&category=power_tools&limit=2&cursor=eyJvIjogMn0=" \
  -H "Authorization: Bearer $STYLUMIA_KEY"
  # Never commit your key — see /docs/key-security
```

The cursor only tracks your position in the result set — it decodes to an
offset, nothing more — it doesn't remember which filters produced it. Drop
or change a filter between calls and you'll walk a different set of rows
from wherever the cursor left off, silently, not as an error. Keep
`retailer`, `category`, `department`, `rcd`, `start_date`, and `end_date`
identical across a whole walk.
