# X-Trader API cursor pagination

URL: https://docs.blizz.id/xtrader/pagination/
Updated: 2026-10-16

X-Trader Public API list endpoints use opaque cursor pagination: each response returns `data.items` and `data.next_cursor`, and the next page is requested by passing `next_cursor` unchanged as the `cursor` query parameter. Pages contain 20 items by default and at most 100 (`limit`). A `null` `next_cursor` marks the last page, and a cursor is valid only for the same token and the same filters.

Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader.

## Paginated endpoints

| Endpoint | Scope | Order |
|---|---|---|
| `GET /sniper/configs` | `sniper:read` | `updated_at`, then `id` |
| `GET /copy-trade/configs` | `copytrade:read` | `updated_at`, then `id` |
| `GET /transfer-snipe/configs` | `transfersnipe:read` | `updated_at`, then `id` |
| `GET /sniper/templates` | `sniper:read` | not stated in the spec |
| `GET /copy-trade/templates` | `copytrade:read` | not stated in the spec |

`GET /labels` is not paginated (see below). The spec states that config lists are ordered by `updated_at` and `id` for stable paging but does not state the direction (ascending or descending).

## Query parameters

### Config lists (sniper, copy-trade, transfer-snipe)

| Parameter | Type | Default | Constraints | Meaning |
|---|---|---|---|---|
| `limit` | integer | 20 | 1-100 | Items per page |
| `cursor` | string | none | max 2048 characters | Opaque signed cursor from the previous page |
| `status` | enum | none | `active`, `paused` | Filter by execution state (`is_active`) |
| `q` | string | none | max 64 characters | Search in `remark` and `target_wallet`, own resources only |
| `label_id` | string | none | label UUID or literal `unlabeled` | Only tasks with this label, or with no label |

A `label_id` UUID that is missing or owned by another account returns `404 not_found`.

### Template lists (sniper, copy-trade)

| Parameter | Type | Default | Constraints | Meaning |
|---|---|---|---|---|
| `limit` | integer | 20 | 1-100 | Items per page |
| `cursor` | string | none | max 2048 characters | Opaque signed cursor, bound to the token and the search query |
| `q` | string | none | max 64 characters | Search template names only |

Template lists have no `status` or `label_id` filter (templates never carry a label).

## Response shape

```json
{
  "data": {
    "items": [ { "id": "...", "version": 3, "is_active": false } ],
    "next_cursor": "opaque-string-or-null"
  },
  "meta": { "request_id": "req_0123456789abcdef" }
}
```

| Field | Type | Meaning |
|---|---|---|
| `data.items` | array | Resources of the page, in the same shape as single `GET` results |
| `data.next_cursor` | string or `null` (required) | Cursor for the next page; `null` when there are no more pages |
| `meta.request_id` | string | Request identifier |

Items are returned as stored snapshots and may contain legacy fields (see [write rules](/xtrader/write-rules/)). List responses do not carry per-item `ETag` headers; use each item's `version` or a single `GET` before writing.

## Cursor rules

- Treat the cursor as opaque: pass it unchanged, never parse, decode or edit it.
- It is bound to the token that produced it. A cursor from one token is not usable with another (including a rotated token, as practical guidance).
- It is bound to the filters (`status`, `q`, `label_id` on config lists; `q` on template lists). After changing any filter, restart from the first page without `cursor`.
- Keep `limit` and filters identical across pages of one traversal.
- The spec does not document cursor expiry or the error code for an invalid cursor. As practical guidance, on a `400` error for a cursor request restart from the first page.

## Examples

First page of paused sniper configs, 50 per page:

```bash
curl 'https://x-trader.ai/api/v1/sniper/configs?limit=50&status=paused' \
  --header "Authorization: Bearer $X_TRADER_API_TOKEN"
```

Next page (the cursor must be URL-encoded):

```bash
curl --get 'https://x-trader.ai/api/v1/sniper/configs' \
  --header "Authorization: Bearer $X_TRADER_API_TOKEN" \
  --data-urlencode 'limit=50' \
  --data-urlencode 'status=paused' \
  --data-urlencode "cursor=$NEXT_CURSOR"
```

Python loop fetching every page:

```python
import os
import requests

BASE = "https://x-trader.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"}

def fetch_all(path, **filters):
    params = {"limit": 100, **filters}
    items = []
    while True:
        r = requests.get(f"{BASE}{path}", headers=H, params=params, timeout=15)
        r.raise_for_status()
        data = r.json()["data"]
        items.extend(data["items"])
        cursor = data["next_cursor"]
        if cursor is None:
            return items
        params = {**params, "cursor": cursor}  # same limit and filters, new cursor

active_copy = fetch_all("/copy-trade/configs", status="active")
unlabeled_snipers = fetch_all("/sniper/configs", label_id="unlabeled")
templates = fetch_all("/sniper/templates", q="night")
print(len(active_copy), len(unlabeled_snipers), len(templates))
```

`requests` URL-encodes `params`, so the cursor is passed through unchanged.

## Labels list

`GET /labels` takes no query parameters and returns every label of the account in `data.items`, with no `next_cursor`. There are at most 20 labels, ordered by `sort_order`, then `created_at`. See [labels](/xtrader/labels/).

## FAQ

### How many items does an X-Trader list return?

20 by default. Set `limit` to any integer from 1 to 100.

### How do I know I reached the last page?

`data.next_cursor` is `null`. Otherwise pass it unchanged as the `cursor` query parameter on the next request.

### Can I change filters while paging?

No. Cursors are bound to the token and the filters of the first request. After changing `status`, `q` or `label_id`, restart from the first page without a cursor.

### Is the labels list paginated?

No. `GET /labels` takes no parameters and returns all labels (at most 20) in `data.items` without `next_cursor`.
