# Paging through results

> List endpoints return up to 1,000 records a page and up to 5,000 records by offset. To read more, page by a field such as created.

Every list endpoint returns its results a page at a time:

- **`limit`** is the number of records in a page: 20 if you leave it out, and at most 1,000.
- **`offset`** is the number of records to skip before the page starts.
- **`offset` + `limit` can't exceed 5,000.** A request beyond that is refused.

So paging by `offset` reaches the first 5,000 records of a result. That covers most uses, such as showing a list a page at a time. To read more than 5,000 records, for example to export or reconcile a whole history, page by a field instead.

## Paging by a field

Sort by a field that doesn't change, and start each page where the last one ended, with `criteria` rather than `offset`. `created` works for every object.

1. Ask for the first page, sorted by `created`, oldest first:

   ```
   GET /transactions?order=created&limit=1000
   ```

2. Note the `created` value of the last record in the page.
3. Ask for the next page from that value, with no `offset`:

   ```
   GET /transactions?order=created&limit=1000&criteria=created>=2026-03-14T09:21:07Z
   ```

4. Skip any records at the start of the page that you already have (match them by `id`), then repeat from step 2 until a page has fewer records than your `limit`.

Use `>=`, not `>`, and skip the records you've already seen: several records can share the same `created` time, and `>` would miss the ones after the page boundary. Keep the rest of your `criteria` the same on every request, combined with commas:

```
criteria=status=SUCCESS,created>=2026-03-14T09:21:07Z
```

Remember to URL encode `criteria` (see [List APIs](/merchants/reference/merchant-instance-api/#list-apis)).

### Example

```javascript
// Reads every successful transaction, a page at a time, without offset
const BASE = `https://app.shuttleglobal.com/c/api/instances/${process.env.SHUTTLE_INSTANCE_KEY}`
const AUTH = `Basic ${Buffer.from(`${process.env.SHUTTLE_SECRET_KEY}:`).toString('base64')}`
const LIMIT = 1000

async function* allTransactions() {
  const seen = new Set()
  let from = null
  while (true) {
    const criteria = ['status=SUCCESS', from && `created>=${from}`].filter(Boolean).join(',')
    const params = new URLSearchParams({ order: 'created', limit: String(LIMIT), criteria })
    const res = await fetch(`${BASE}/transactions?${params}`, { headers: { Authorization: AUTH } })
    if (!res.ok) throw new Error(`Shuttle returned ${res.status}`)
    const { transactions } = await res.json()
    for (const t of transactions) {
      if (seen.has(t.id)) continue
      seen.add(t.id)
      yield t
    }
    if (transactions.length < LIMIT) return
    from = transactions[transactions.length - 1].created
  }
}

for await (const t of allTransactions()) console.log(t.id, t.amount, t.currency)
```

For a very long history, keep only the ids that share the latest `created` time rather than every id you've seen.

## Which field to page by

- **`created`** suits most cases: it never changes once a record exists.
- **Avoid fields that change**, such as `updated` or `status`: a record that changes while you page can move to a page you've already read, or appear twice.
- **To read newest first**, sort with `order=created:desc` and use `created<=` instead of `created>=`.