Docs menu

Pagination

Every list endpoint returns one page at a time, newest first, with an opaque cursor for the next page.

The page envelope

{
  "data": [{ "id": "…" }, { "id": "…" }],
  "next_cursor": "eyJpZCI6IjAxOTljM2EyLTRiN2UtN2YzMC05YzJlLTZhOGQ0ZjFiMmMwMSIsInRzIjoi…",
  "has_more": true
}
ParameterDefaultNotes
limit501 to 100. Below 1 counts as 1, above 100 as 100.
cursor—The next_cursor of the previous page. Leave it out for the first page.

Items are ordered by creation time, newest first, to the microsecond — finer than the whole seconds created_at shows, so items that appear to share a created_at can come back in any id order; only items created in the very same microsecond are ordered by id. The cursor marks a position in that order, so new items created while you page do not shift or repeat what you have already read — they appear on a fresh first page.

Treat the cursor as opaque: store it, pass it back unchanged, never build one. A cursor that cannot be read is a 422 — detail_key errors.integrations.<resource>.invalid_cursor on the resource lists (errors.integrations.invoices.invalid_cursor, …) and errors.integrations.invalid_cursor on the webhook deliveries list; start again without it.

Reading everything

async function* allInvoices(apiKey) {
  let cursor;
  do {
    const url = new URL('https://api.officehero.io/v1/invoices');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
    if (!res.ok) throw new Error(`${res.status} ${(await res.json()).detail_key}`);
    const page = await res.json();
    yield* page.data;
    cursor = page.has_more ? page.next_cursor : undefined;
  } while (cursor);
}
import requests

def all_invoices(api_key):
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        res = requests.get(
            "https://api.officehero.io/v1/invoices",
            params=params,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=30,
        )
        res.raise_for_status()
        page = res.json()
        yield from page["data"]
        if not page["has_more"]:
            return
        cursor = page["next_cursor"]
<?php
function allInvoices(string $apiKey): Generator
{
    $cursor = null;
    do {
        $query = http_build_query(array_filter(['limit' => 100, 'cursor' => $cursor]));
        $ch = curl_init("https://api.officehero.io/v1/invoices?{$query}");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}"],
        ]);
        $page = json_decode(curl_exec($ch), true, 512, JSON_THROW_ON_ERROR);
        yield from $page['data'];
        $cursor = $page['has_more'] ? $page['next_cursor'] : null;
    } while ($cursor !== null);
}

No filters or sorting yet

In this version lists have no filters (no updated_since, no status=) and no other sort order. To keep a copy in sync, either:

  • subscribe to webhooks for the resources you mirror and fetch the changed item by id, or
  • page from the newest item until you reach items you already have.

Exceptions

GET /v1/webhook_endpoints is not paginated — it returns all of your endpoints at once as { "data": [...] }.