Skip to content
DevelopersGet an API key

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

sortOrder
-created_atNewest first. The default, and what Zapier and Make expect.
created_atOldest first.
-updated_atMost recently changed first.
updated_atLeast 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_at is when they were recorded.
  • created_since works the same way for new records only. Both take ISO 8601 times, like 2026-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.
NextErrors and rate limits