Pagination and syncing
Lists come a page at a time. Follow the cursor to read everything, then ask only for what changed.
Pages
Every list returns up to limit records (25 unless you ask for up to 100) and tells you whether there are more:
JSON
{
"object": "list",
"data": [ { "id": "…", "object": "load", … } ],
"has_more": true,
"next_cursor": "eyJpIjogIjFkNTkyOTBkLTdlMjMtNDZm…"
}To get the next page, send the same request again, with the same filters, plus cursor set to next_cursor. When has_more is false, next_cursoris null and you're done. A cursor never skips or repeats a record, even when new ones arrive while you read.
Order
sort | Order |
|---|---|
-created_at | Newest first. The default, and what Zapier and Make expect. |
created_at | Oldest first. |
-updated_at | Most recently changed first. |
updated_at | Least recently changed first. The one to use for syncing. |
Keeping another system in sync
Every record has updated_at. To copy changes into another system, remember when your last sync started, then ask for records changed since then, oldest change first, and follow the cursor:
// Read every invoice changed since the last sync, oldest change first.
const startedAt = new Date().toISOString();
const url = new URL("https://harlo-api-dev.pages.dev/v1/invoices");
url.searchParams.set("limit", "100");
url.searchParams.set("sort", "updated_at");
url.searchParams.set("updated_since", lastSync ?? "2020-01-01T00:00:00Z");
while (true) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.HARLO_API_KEY}` } });
if (res.status === 429) { await sleep(Number(res.headers.get("retry-after")) * 1000); continue; }
if (!res.ok) throw new Error((await res.json()).error.message);
const page = await res.json();
for (const invoice of page.data) await upsert(invoice); // your code
if (!page.has_more) break;
url.searchParams.set("cursor", page.next_cursor); // same filters, next page
}
lastSync = startedAt; // saved for next time- Save the time the run started, not when it finished, so changes made while it ran are picked up next time. Seeing a record twice is harmless if you update by
id. - An invoice's
updated_atmoves when it's paid, part-paid or voided, so the same loop catches payments. - Payments and events never change after they're made; for them,
updated_atis when they were recorded. created_sinceworks the same way for new records only. Both take ISO 8601 times, like2026-11-01T00:00:00Z.
Following activity
/v1/eventsis the workspace's activity log: one record per change, with a sentence saying what happened. Filter it with entity_type and entity_id to follow one load.