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

# Pagination

> How to page through list endpoints using cursor pagination.

The list endpoints in the VEXiON cards API use **cursor-based pagination**. Numeric offsets are not supported, because the underlying indexes can reshuffle and cause skipped or duplicated records.

## Response envelope

Every list endpoint returns the same envelope. `GET /v1/webhook_endpoints/event_types` returns one complete list in a single response, with `has_more: false` and `next_cursor: null`:

```json theme={null}
{
  "object": "list",
  "url": "/v1/customers",
  "data": [ /* ...items... */ ],
  "has_more": true,
  "next_cursor": "cus_iofv5j4hn6bphinmmyat27tc5y"
}
```

| Field | Description |
| - | - |
| `object` | Always `"list"` for list responses |
| `url` | Stable URL for this collection |
| `data[]` | Array of the requested resource type |
| `has_more` | `true` when more pages exist after this one |
| `next_cursor` | The cursor for the next page, or `null` when this page ends the collection. Always present. |

> [!TIP]
>
> **Use `next_cursor`.** Pass it straight back to the same parameter you used and you never have to know which field is the cursor, or which direction the list sorts in. `has_more: true` always comes with a non-null `next_cursor`, and the two never disagree.
>
> Deriving the cursor yourself from the last item's `id` still works and is unchanged, but it puts that burden on you, and it is wrong on `GET /v1/icons`, whose cursor is the icon `slug`, not an object id.

## Cursor parameters

| Parameter | Direction | Behavior |
| - | - | - |
| `starting_after` | Forward | Returns items STRICTLY AFTER the given ID (next page) |
| `ending_before` | Reverse | Returns items STRICTLY BEFORE the given ID (previous page). Not available on `GET /v1/icons`, which pages forward only |
| `limit` | n/a | Page size, 1-100 (default 25) |

> [card] Paging forward
>
> Send `starting_after` set to `next_cursor` from the current page.

> [card] Paging backward
>
> Send `ending_before` set to `next_cursor` from the current page.

`next_cursor` continues **the direction you are already travelling**. Page forward and it points at the next older page; page backward with `ending_before` and it points at the next newer one. Feed it back to whichever parameter you used.

## Pagination pattern

The pattern: fetch a page, pass its `next_cursor` as `starting_after` on the next request, and stop when `next_cursor` comes back `null`.

Here is that walk end to end - three requests against `/v1/customers`, the cursor carried forward each time, until `has_more` finally comes back `false`:

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E5F6FF','primaryBorderColor':'#36C6FA','primaryTextColor':'#0B3A4A','lineColor':'#2BA8D9','secondaryColor':'#F4FCFF','tertiaryColor':'#FFFFFF','fontFamily':'Manrope, ui-sans-serif, system-ui, sans-serif'}}}%%
sequenceDiagram
    participant Client
    participant API as VEXiON cards API

    Client->>API: GET /v1/customers?limit=100
    API-->>Client: data: 100 items<br/>has_more: true<br/>next_cursor: cus_101

    Note over Client: cursor = cus_101

    Client->>API: GET /v1/customers?limit=100<br/>&starting_after=cus_101
    API-->>Client: data: 100 items<br/>has_more: true<br/>next_cursor: cus_202

    Note over Client: cursor = cus_202

    Client->>API: GET /v1/customers?limit=100<br/>&starting_after=cus_202
    API-->>Client: data: 37 items<br/>has_more: false<br/>next_cursor: null

    Note over Client: next_cursor is null - stop paging
```

**1. Request the first page**

Call the endpoint with no `starting_after`. Set `limit` if you want a page size other than the default 25.

**2. Read next\_cursor**

If `next_cursor` is not `null`, more pages exist and that value is your cursor. You do not need to look at the items at all.

**3. Request the next page**

Repeat the same request, adding `starting_after=<next_cursor>`.

**4. Stop when next\_cursor is null**

A final page carries `has_more: false` and `next_cursor: null`. The API never returns the cursor you just sent, so a loop on `next_cursor` always terminates.

```javascript Node.js theme={null}
let cursor;
const all = [];
do {
  const url = new URL('https://api.vexioncards.one/v1/customers');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('starting_after', cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.VXN_API_KEY}` }
  });
  const page = await res.json();

  all.push(...page.data);
  cursor = page.next_cursor;
} while (cursor);

console.log(`Fetched ${all.length} customers`);
```

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

url = "https://api.vexioncards.one/v1/customers"
headers = {"Authorization": f"Bearer {os.environ['VXN_API_KEY']}"}
params = {"limit": 100}
all_customers = []

while True:
    r = requests.get(url, headers=headers, params=params)
    page = r.json()
    all_customers.extend(page["data"])
    if page["next_cursor"] is None:
        break
    params["starting_after"] = page["next_cursor"]

print(f"Fetched {len(all_customers)} customers")
```

```bash cURL theme={null}
# First page
curl -G "https://api.vexioncards.one/v1/customers" \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -d "limit=100"

# Next page (use next_cursor from the previous response)
curl -G "https://api.vexioncards.one/v1/customers" \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -d "limit=100" \
  -d "starting_after=cus_..."
```

## Sort order

List endpoints return records newest first, in the order the records were created. A record that was imported or back-dated can show a `created_at` that is out of sequence with the rest.

The one exception is `GET /v1/icons`. It is a static, centrally curated catalog rather than a record of your own data, so it has no `created_at` and is ordered by `slug` ascending.

`starting_after` and `ending_before` are **mutually exclusive**: send exactly one. A request carrying both is rejected with `400 invalid_query_parameter` rather than being resolved in favour of either.

## Errors

| Code | HTTP | When |
| - | - | - |
| `invalid_limit` | 400 | `limit` is outside 1-100, or not an integer. Rejected, not clamped - there is no `X-Pagination-Clamped` header. |
| `invalid_cursor` | 400 | `starting_after` or `ending_before` does not decode to a valid id for this collection. |
| `invalid_query_parameter` | 400 | Both `starting_after` and `ending_before` were sent on the same call. |

`param` names the offending query parameter on all three codes.

Filters (`status=active`, `created[gte]=...`, and so on) compose with cursor pagination: the cursor identifies your position within the filtered, sorted result, not within the whole collection.

> [!WARNING]
>
> **Never use numeric offsets.** The underlying index can reshuffle between requests (new inserts, concurrent updates), causing you to skip records or fetch duplicates. Cursor pagination is the only supported pattern.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.