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
}
| Parameter | Default | Notes |
|---|---|---|
limit | 50 | 1 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": [...] }.