# Pagination

> Cursor pagination on GET /v1/shipments. Follow nextCursor until it is null.

`GET /v1/shipments` returns one page at a time, most recently updated first:

```json
{
  "data": [{ "reference": "QB51907", "status": "Active", "…": "…" }],
  "nextCursor": "eyJ1IjoiMjAyNi0wOS0xNVQyMTo0ODowMFoiLCJyIjoiUUI1MTkwNyJ9"
}
```

| Parameter | Meaning |
|---|---|
| `limit` | Page size, 1–100. Default 50. |
| `cursor` | The `nextCursor` value of the previous response, unchanged. |

## Rules

- Next page: repeat the request with the **same filters** and `cursor` set to the previous `nextCursor`.
- `nextCursor: null` means this is the last page.
- Cursors are opaque.
- An unusable cursor returns `400 invalid_cursor`. Restart without `cursor`.
- There is no total count and no page numbers.
- A shipment that changes while you paginate moves to the front, so it can appear twice or be skipped within one run. Upsert by `reference` and [poll with `updatedSince`](https://developers.gocubic.io/recipes.md#poll-for-changes).

## Filters

All filters are optional and combine with AND.

| Parameter | Meaning |
|---|---|
| `status` | One of `Active`, `Delivered`, `Cancelled`. Repeat it to match any of several: `?status=Active&status=Delivered`. |
| `updatedSince` | ISO 8601 timestamp. Shipments whose `updatedAt` is at or after it. |
| `createdSince` | ISO 8601 timestamp. Shipments whose `createdAt` is at or after it. |
| `tag` | Exact match on one of the customer's own tags (PO numbers). |
| `search` | Free text across the reference, tags, master bill / air waybill numbers, container numbers, and Amazon FBA and reference IDs. |

An invalid value returns `400 invalid_parameter` naming the `parameter`; the `hint` lists the allowed values.

## Read every page

```ts
const BASE_URL = 'https://api.gocubic.io/v1';
const headers = { Authorization: `Bearer ${process.env.CUBIC_API_KEY}` };

const shipments = [];
let cursor: string | null = null;
do {
  const url = new URL(`${BASE_URL}/shipments`);
  url.searchParams.set('limit', '100');
  url.searchParams.set('status', 'Active');
  if (cursor) url.searchParams.set('cursor', cursor);

  const response = await fetch(url, { headers });
  if (!response.ok) throw new Error(JSON.stringify(await response.json()));
  const page = await response.json();

  shipments.push(...page.data);
  cursor = page.nextCursor;
} while (cursor);

console.log(`${shipments.length} active shipments`);
```

```python
import os

import requests

BASE_URL = "https://api.gocubic.io/v1"
headers = {"Authorization": f"Bearer {os.environ['CUBIC_API_KEY']}"}

shipments = []
params = {"limit": 100, "status": "Active"}
while True:
    response = requests.get(f"{BASE_URL}/shipments", headers=headers, params=params, timeout=30)
    response.raise_for_status()
    page = response.json()
    shipments.extend(page["data"])
    if page["nextCursor"] is None:
        break
    params["cursor"] = page["nextCursor"]

print(f"{len(shipments)} active shipments")
```

The client in [Recipes](https://developers.gocubic.io/recipes.md#client) wraps this loop and adds 429 backoff.
