Pagination
List endpoints return results in pages. Every paginated response uses the same envelope, so once you've built a helper that walks one list, you can point it at any list endpoint and it works. This page covers the query parameters, the response shape, and two patterns for iterating to the end of a list.
Query parameters
Two parameters control paging on every list endpoint.
| Parameter | Type | Description |
|---|---|---|
page |
integer | The page number to retrieve, 1-indexed. Defaults to 1. |
per_page |
integer | The number of results per page. Defaults vary per endpoint (typically 15) and the server caps the maximum. Always read meta.per_page to confirm what the server applied. |
curl "https://api.try.be/sites?page=2&per_page=50" \
-H "Authorization: Bearer YOUR_API_KEY"
If you ask for more than the cap, the response is silently capped — the request
succeeds, but meta.per_page reflects the applied value, not the requested one.
Always read it before deciding "did we get everything we asked for?".
Not every list endpoint paginates. A handful of small, bounded lookups (e.g.
supported currencies, supported locales) return their full result set in one
shot, with no meta or links block. Treat the response shape as the source of
truth — if meta and links are present, the endpoint paginates; if they
aren't, you've already got everything.
Response shape
A paginated list response wraps three top-level keys:
{
"data": [
{ "id": "…", "name": "Palm Tree Spa", "…": "…" }
],
"links": {
"first": "https://api.try.be/sites?page=1",
"next": "https://api.try.be/sites?page=2",
"prev": null,
"last": "https://api.try.be/sites?page=5"
},
"meta": {
"current_page": 1,
"from": 1,
"to": 15,
"total": 73,
"per_page": 15,
"last_page": 5
}
}
data
The page of results. Always an array, even when the page is empty or contains a single item. The item shape depends on the endpoint; see the endpoint reference for the type.
meta
Counts and positional information for the current page.
| Field | Type | Meaning |
|---|---|---|
current_page |
integer | The page number of this response. |
last_page |
integer | The page number of the final page. Useful for "page N of M" UI. |
from |
integer | The 1-indexed position of the first item on this page within the full result set. |
to |
integer | The 1-indexed position of the last item on this page. |
total |
integer | The total number of items across all pages. |
per_page |
integer | The page size the server actually applied (which may be less than what you asked for, if you exceeded the cap). |
links
Fully-qualified URLs for navigating the result set. Each URL preserves the
original query string (filters, sort, page size) and only swaps the page
parameter.
| Field | Type | Meaning |
|---|---|---|
first |
string | URL of page 1. Always present. |
last |
string | URL of the last page. Always present. |
next |
string | null | URL of the next page, or null if you're on the last page. |
prev |
string | null | URL of the previous page, or null if you're on the first page. |
Because links.next is fully-qualified — including the original filters and
per_page — you can hand it straight to your HTTP client without rebuilding
the query string yourself.
Iterating a list
There are two reasonable ways to walk a list to the end. Both work; pick whichever fits your code.
Style 1: follow links.next
The simplest pattern. Keep fetching until links.next is null.
async function* iterateSites(client) {
let url = '/sites?per_page=100'
while (url) {
const res = await client.get(url)
for (const site of res.data) yield site
url = res.links.next
? res.links.next.replace('https://api.try.be', '')
: null
}
}
for await (const site of iterateSites(client)) {
// process one site at a time, regardless of total page count
}
This style is robust to changes in total while you're iterating (e.g. another
process adding rows) and doesn't require you to track page numbers.
Style 2: loop on meta.last_page
If you want to parallelise page fetches or display a progress bar, drive the
loop off meta.
async function fetchAllSites(client) {
const first = await client.get('/sites?per_page=100')
const results = [...first.data]
const pageRequests = []
for (let page = 2; page <= first.meta.last_page; page++) {
pageRequests.push(client.get(`/sites?per_page=100&page=${page}`))
}
for (const res of await Promise.all(pageRequests)) {
results.push(...res.data)
}
return results
}
The downsides: last_page reflects state at the moment of the first request,
so concurrent writes during the walk can give you a stale stopping point or
duplicates. Use this style when you need parallelism and you know the data
isn't changing underneath you.
The same shape in PHP, following links.next:
function iterateSites(GuzzleHttp\Client $client): Generator {
$url = '/sites?per_page=100';
while ($url !== null) {
$body = json_decode(
(string) $client->get($url)->getBody(),
true
);
foreach ($body['data'] as $site) {
yield $site;
}
$url = $body['links']['next']
? parse_url($body['links']['next'], PHP_URL_PATH)
. '?' . parse_url($body['links']['next'], PHP_URL_QUERY)
: null;
}
}
An empty page
A list with no results still has the envelope — data is an empty array,
total is 0, and links.next is null. You don't need a special case for
"no results"; the iterator loops zero times.
{
"data": [],
"links": {
"first": "https://api.try.be/sites?page=1",
"last": "https://api.try.be/sites?page=1",
"next": null,
"prev": null
},
"meta": {
"current_page": 1,
"last_page": 1,
"from": null,
"to": null,
"total": 0,
"per_page": 15
}
}
Choosing per_page
Bigger pages mean fewer round-trips and lower total latency, but also more work per request and higher memory use on both ends. A few rules of thumb:
- For interactive paging in a UI: leave
per_pageat the default. Users rarely look past page 3. - For bulk export or sync jobs: request the maximum the endpoint allows. Read
meta.per_pageto discover the cap empirically the first time, then stick with that value. - For sub-second polling loops: use the smallest
per_pagethat satisfies your consumer, and consider webhooks instead.
Caveats
- Sort order matters when paging concurrently with writes. If items can be
inserted into the middle of the sort order while you're walking the list,
you'll see duplicates or skips. Where it's available, sort by
created_atascending — newly created items only appear at the end, so a forward walk stays consistent. totalis a count at request time. Don't treat it as a covenant for the rest of the walk.- Filters are part of the page identity.
?status=active&page=2and?page=2describe different pages. ThelinksURLs preserve your filters so you don't have to remember this — but if you build URLs yourself, do.
Bulk exports (queued reports)
Some reports are too large to paginate response-by-response — revenue reports, customer dumps, transaction history. These ship behind a queued-job pattern: you POST to kick the export off, poll a job endpoint for completion, then fetch the result from a short-lived S3 URL.
POST /shop/reports/<report>/queue → returns a queued_job id
GET /customers/queued-jobs/{id} → status: pending / complete / failed
when complete, output.download_url
points at a signed S3 link
The signed link expires fast (minutes) and the S3 hostname can
move during region migrations. If your environment uses an
egress allowlist, ask your Onboarding Manager for the current
host pattern (typically *.s3.<region>.amazonaws.com) rather
than pinning a literal.
Things worth knowing before you wire up an automated export:
- Reports are capped at ~10,000 rows per job. Drive the date
window down via
date_from/date_toif you need everything; loop the query in batches and dedupe by primary key. - Filters apply to body rows, not always to summary headers.
A filter like
?type=refundcorrectly filters the data rows in the CSV — but on a handful of reports the currency / total header at the top of the file reverts to the unfiltered default. Treat headers as advisory; recompute from the data rows if accuracy matters. - Column set is fixed per report. There's no API knob to add / remove columns from a queued export. If your destination needs a narrower or wider shape, transform the CSV on your side.
See also
- Getting started — the first request you'll make hits a paginated endpoint.
- Errors — list endpoints return the same error shapes as everything else; pagination doesn't add new ones.
- Rate limits — relevant if you parallelise a big walk.