Guide

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).

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_page at the default. Users rarely look past page 3.
  • For bulk export or sync jobs: request the maximum the endpoint allows. Read meta.per_page to discover the cap empirically the first time, then stick with that value.
  • For sub-second polling loops: use the smallest per_page that 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_at ascending — newly created items only appear at the end, so a forward walk stays consistent.
  • total is 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=2 and ?page=2 describe different pages. The links URLs 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_to if 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=refund correctly 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.