> ## Documentation Index
> Fetch the complete documentation index at: https://docs.compago.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> How to page through list endpoints without missing or repeating rows

## Two styles, and when you meet each

Compago has two generations of list endpoints, and they page differently. The style is a property of the endpoint, not something you choose per request.

| Endpoints | Style | Parameters |
| - | - | - |
| `/developer/v1/...` (Developer API) | Cursor | `limit`, `cursor` |
| `/one-time-payment`, `/payment-method`, `/payment-method/{id}/payment` | Offset | `page`, `pageSize` |

Every list on the Developer API is cursor based. The offset endpoints are stable and are not going away.

## Cursor pagination (Developer API)

<Note>
  These examples call the production host. While you build, replace `https://api-harmony.compago.com` with `https://demo-api-harmony.compago.com` and use a key from the Demo dashboard. See [Environments](/get-started/environments).
</Note>

Ask for a page, then follow the cursor the response hands back.

```bash theme={null}
curl "https://api-harmony.compago.com/api/developer/v1/payment?limit=50" \
  -H "x-api-key: $COMPAGO_API_KEY"
```

Every list response has the same envelope:

```json theme={null}
{
  "data": [
    { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "status": "CONFIRMED", "amount": 1500 }
  ],
  "pagination": {
    "limit": 50,
    "hasMore": true,
    "nextCursor": "eyJ0IjoiMjAyNi0wMy0wNFQwNTowNjowNy4wODlaIiwiaWQiOiIxMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUifQ"
  }
}
```

To fetch the next page, pass `nextCursor` back as `cursor`. Stop when `nextCursor` is `null`.

```bash theme={null}
curl "https://api-harmony.compago.com/api/developer/v1/payment?limit=50&cursor=eyJ0Ijoi..." \
  -H "x-api-key: $COMPAGO_API_KEY"
```

<CodeGroup>
  ```javascript Node.js theme={null}
  async function* allPayments(apiKey) {
    let cursor = null;

    do {
      const url = new URL('https://api-harmony.compago.com/api/developer/v1/payment');
      url.searchParams.set('limit', '100');
      if (cursor) url.searchParams.set('cursor', cursor);

      const res = await fetch(url, { headers: { 'x-api-key': apiKey } });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);

      const { data, pagination } = await res.json();
      yield* data;
      cursor = pagination.nextCursor;
    } while (cursor);
  }
  ```

  ```python Python theme={null}
  import requests

  def all_payments(api_key):
      cursor = None
      while True:
          params = {"limit": 100}
          if cursor:
              params["cursor"] = cursor

          res = requests.get(
              "https://api-harmony.compago.com/api/developer/v1/payment",
              params=params,
              headers={"x-api-key": api_key},
              timeout=30,
          )
          res.raise_for_status()
          body = res.json()

          yield from body["data"]

          cursor = body["pagination"]["nextCursor"]
          if not cursor:
              return
  ```
</CodeGroup>

### Rules worth knowing

<AccordionGroup>
  <Accordion title="limit is capped at 100">
    `limit` accepts 1 to 100 and defaults to 20. A larger value is **rejected with a 400**, not silently reduced, so a client asking for 1,000 rows finds out rather than quietly looping on pages of 100.
  </Accordion>

  <Accordion title="The cursor is opaque">
    Treat `nextCursor` as a string with no internal meaning. Do not parse it, build one, or reuse a cursor from a different endpoint. A cursor Compago did not issue is rejected with a 400 rather than being ignored, because a cursor that silently fell back to page one would turn a paging loop into an infinite one.
  </Accordion>

  <Accordion title="Ordering is newest first">
    Rows come back by creation date descending, with the id breaking ties. The tie-break matters: without it, rows created in the same millisecond could be served twice or skipped entirely.
  </Accordion>

  <Accordion title="There is no total count">
    The envelope reports `hasMore`, not a total. Counting every matching row on each request means a full scan that gets slower as your history grows, so the endpoints do not do it. If you need a total, page through and count.
  </Accordion>

  <Accordion title="New rows during a long crawl">
    The cursor walks backwards from where you started, so rows created **while** you are paging are simply not part of the walk. To pick them up, start a fresh crawl and stop when you reach an id you have already seen, or filter on `createdAtFrom`.
  </Accordion>
</AccordionGroup>

## Offset pagination (unversioned endpoints)

The older endpoints take `page` (from 1) and `pageSize` (up to 500), and return the total alongside the items:

```json theme={null}
{
  "total": 213,
  "page": 1,
  "pageSize": 100,
  "items": [{ "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" }]
}
```

<Warning>
  Offset pages are not stable while rows are being created. A payment saved between your request for page 1 and your request for page 2 shifts every later row down by one, so a row can appear twice or not at all. For anything that has to be complete, such as reconciliation, use the Developer API.
</Warning>

## Filtering instead of paging

Most list endpoints accept filters, and filtering is almost always cheaper than paging through everything and discarding rows client side. `GET /developer/v1/payment` alone accepts `status`, `type`, `operationCode`, `createdAtFrom`, `createdAtTo`, and the ids of the payment link, subscription, one-time payment or saved card the payment came from.

Filters that accept several values are repeated:

```bash theme={null}
curl "https://api-harmony.compago.com/api/developer/v1/payment?status=CONFIRMED&status=REFUNDED" \
  -H "x-api-key: $COMPAGO_API_KEY"
```

<Note>
  The Developer API deliberately does not offer a free-text search parameter. Free-text matching cannot use an index, and one such query looping against production would compete for the same database connections your checkouts need. Filter on the structured fields instead.
</Note>
