Pagination
Every list endpoint is cursor-paginated. There is no offset.
List endpoints return a uniform envelope:
{
"object": "list",
"data": [ /* … */ ],
"has_more": true,
"next_cursor": "MjAyNi0wNy0xNVQwOTozMToxMi4wMDBafDNmMWMyZTAw"
}data— the page, newest first.has_more— whether another page exists.next_cursor— pass it back as?cursor=for the next page.nullmeans you've reached the end.
Parameters
| Parameter | Default | Notes |
|---|---|---|
limit | 50 | 1–100. Anything outside that range is a 422. |
cursor | — | Opaque. Only ever pass back a next_cursor you received. |
Why cursors, not pages
?page=2 breaks under a moving dataset: if three leads are updated while you're iterating, rows shift between pages and you silently skip some. A cursor pins the exact position in the sort order, so a lead updated mid-iteration reappears at the front rather than displacing the page you haven't read yet.
That is also why a malformed cursor is a 422 rather than a silent reset to page one — quietly restarting an iteration is how integrations double-process records.
Iterating
async function* allLeads(token: string) {
let cursor: string | null = null;
do {
const url = new URL("https://replyfirst.ae/api/v1/leads");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error((await res.json()).error);
const page = await res.json();
yield* page.data;
cursor = page.next_cursor;
} while (cursor);
}
for await (const lead of allLeads(process.env.RF_ORG_TOKEN!)) {
console.log(lead.id, lead.qualification_stage);
}Incremental sync
Don't re-walk the whole list on every sync. Record the newest updated_at you've seen, then pass it as updated_after on the next run:
curl "https://replyfirst.ae/api/v1/leads?updated_after=2026-07-26T00:00:00Z&limit=100" \
-H "Authorization: Bearer $RF_ORG_TOKEN"Better still, let the webhook push changes to you and use updated_after only to backfill after downtime.