Pagination
The Proxio API uses cursor pagination everywhere. Pass limit and cursor, read meta.next_cursor, and loop until it's null. Includes a copy-paste pagination loop in Python and Node.js.
Every list in the Proxio API is cursor-paginated. There is no page number and no offset, you pass an opaque cursor forward and stop when the API says there's nothing left.
Parameters
| Parameter | Values | Default | Notes |
|---|---|---|---|
limit | 1 to 100 | 20 | Maximum items per page. |
cursor | opaque string | - | The next_cursor from the previous page. Omit it for the first page. |
Results are ordered newest first (created_at descending, then id
descending), a stable order that never skips or repeats an item as you page.
Reading the cursor
Paginated responses put the next cursor in meta.next_cursor. When it's a
string, there's another page; when it's null, you've reached the end.
{
"data": [
{ "id": "clord_9f2a", "status": "PAID", "total": "112.50", "created_at": "2026-07-16T10:00:00.000Z" }
],
"meta": {
"next_cursor": "eyJpZCI6ImNsb3JkXzlmMmEiLCJ0cyI6IjIwMjYtMDctMTZUMTA6MDA6MDBaIn0",
"has_more": true,
"request_id": "req_8Ke2jP4mQ"
}
}meta.has_more says the same thing as next_cursor in boolean form, true
exactly when next_cursor is a string and false exactly when it's null.
Use whichever reads more naturally in your loop condition, they never
disagree.
Cursors are opaque, pass them back verbatim
A cursor is an encoded pointer, not something you construct. Send back exactly
the next_cursor string you received. Editing or truncating it fails with
INVALID_CURSOR.
There is no total count: paginate until next_cursor is null (or
has_more is false) rather than computing a page count up front.
Sorting and date filters
A cursor list's default order is newest first (created_at descending). Some
lists accept more:
| Parameter | Values | Notes |
|---|---|---|
sort | a sort key, list-dependent | Defaults to created_at. An endpoint that supports more names them on its own page, for example orders sorts by created_at or total and wallet transactions by created_at or amount. /services accepts only created_at, there's no second sort key to ask for. An unrecognized sort fails with VALIDATION_ERROR naming the allowed set, it's never silently ignored. |
order | asc | desc | Defaults to desc. Same validation as sort. |
created_after, created_before | ISO 8601 timestamps | Both inclusive, on the list's own created_at. An unparseable value, or a before earlier than after, fails with VALIDATION_ERROR naming the specific parameter. |
Sort, order, and the cursor
A cursor is only valid under the ordering it was issued for
A cursor encodes a position, and a position only means something inside
one specific ordering. Change sort or order partway through a walk and
the API refuses the old cursor with
INVALID_CURSOR rather than silently
serving rows twice or skipping some, it does not transparently restart
you. To change sort or order, drop cursor entirely and start a fresh
walk from page one. A cursor from before a list supported sorting keeps
working under the default created_at / desc order, only an actual change
of sort or order mid-walk is refused.
Which lists paginate
Lists that can grow without bound are cursor-paginated:
/services, /orders,
/wallet/transactions,
/wallet/topups,
/webhooks, /api-keys,
/webhooks/{id}/deliveries, and
/events.
Small, bounded lists return a full array in data (still with meta, but no
cursor): /locations,
/products, a service's
credentials (up to 20), a credential's
whitelist (up to 50), and its
sessions (up to 500).
A pagination loop
Keep calling with the returned cursor until it comes back null, collecting
every page into one list:
import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def paginate(path, params=None):
params = dict(params or {})
params.setdefault("limit", 100)
cursor = None
while True:
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=15)
resp.raise_for_status()
body = resp.json()
for item in body["data"]:
yield item
cursor = body["meta"].get("next_cursor")
if not cursor:
break
orders = list(paginate("/orders", {"status": "PAID"}))
print(f"Fetched {len(orders)} orders")const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const HEADERS = { Authorization: `Bearer ${API_KEY}` }
async function paginate(path, params = {}) {
const items = []
const query = new URLSearchParams({ limit: "100", ...params })
let cursor = null
do {
if (cursor) query.set("cursor", cursor)
const res = await fetch(`${BASE}${path}?${query}`, { headers: HEADERS })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const body = await res.json()
items.push(...body.data)
cursor = body.meta.next_cursor
} while (cursor)
return items
}
const orders = await paginate("/orders", { status: "PAID" })
console.log(`Fetched ${orders.length} orders`)Use the largest limit (100) to minimize round trips, and remember each page is
one request against your rate-limit budget.
Related pages
Rate Limits
Proxio API rate limits are documented and per-key. 120 requests per minute by default, a per-key override, and 60 per minute for unauthenticated discovery. Every response carries X-RateLimit-* headers, and 429s include Retry-After.
Idempotency
Send an Idempotency-Key on Proxio API mutations so retries never double-charge or double-create. Learn the new / replay / conflict / in-progress semantics, which endpoints require the key, the 24-hour TTL, and the Idempotent-Replay header.

