Parselv1.0.0

Pagination

How list responses, page metadata, and truncation flags work across the Parsel API.

Paginated list endpoints in the Parsel API return a uniform { data, metadata } envelope. This page documents the shared shape and how to iterate through pages, using the Billing API as the running example.

The list envelope

Every list endpoint wraps its rows in data and its page state in metadata. For example, GET /billing/invoices/{id}/line_items:

{
  "data": [
    /* zero or more items */
  ],
  "metadata": {
    "current_page": 1,
    "page_size": 10,
    "total_count": 35,
    "total_pages": 4
  }
}
FieldTypeMeaning
current_pageintegerThe 1-indexed page returned.
page_sizeintegerItems per page. Defaults to 50.
total_pagesintegerTotal number of pages at the current page_size.
total_countintegerTotal items across all pages.

Pass ?page=N&page_size=M on the request to control the slice. Defaults are page=1 and page_size=50.

Iterate through every page

When you need every record — for an export, a reconciliation job, or a backfill — increment page until you've consumed all total_pages.

async function fetchAll() {
  const all = [];
  let page = 1;
  while (true) {
    const url = new URL("https://api.parsel.app/billing/invoices");
    url.searchParams.set("page", String(page));
    url.searchParams.set("page_size", "100");
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.PARSEL_TOKEN}` },
    });
    if (!res.ok) throw new Error(`Parsel API ${res.status}`);
    const { data, metadata } = await res.json();
    all.push(...data);
    if (metadata.current_page >= metadata.total_pages) break;
    page += 1;
  }
  return all;
}
def fetch_all():
    all_rows = []
    page = 1
    while True:
        resp = requests.get(
            "https://api.parsel.app/billing/invoices",
            headers={"Authorization": f"Bearer {os.environ['PARSEL_TOKEN']}"},
            params={"page": page, "page_size": 100},
        )
        resp.raise_for_status()
        body = resp.json()
        all_rows.extend(body["data"])
        if body["metadata"]["current_page"] >= body["metadata"]["total_pages"]:
            break
        page += 1
    return all_rows

The same loop pattern works for any paginated endpoint.

Single-resource envelope

Single-resource endpoints (e.g. GET /billing/invoices/{id}) wrap the resource in data with no metadata block:

{
  "data": {
    "id": "0a6bb80e-3d62-4663-b82f-11840b83850b",
    "invoice_number": "INV-VXW8LQ-20260401-20260415",
    "status": "OPEN"
  }
}

Truncation signals

Some endpoints inline a related collection up to a fixed cap and surface a boolean flag when truncated. The Billing API's show endpoint, for instance, inlines up to 250 line items and sets line_items_has_next_page: true when the invoice has more. When that flag is true, switch to the dedicated paginated endpoint (GET /billing/invoices/{id}/line_items) to read the remainder.

The flag and the 250-item cap are specific to billing line items, but the pattern — inline-with-cap plus a *_has_next_page flag — is the convention any future endpoint will follow.

Empty pages

A page value past total_pages returns 200 with data: []. There is no special "out of range" error.

On this page