Pagination
Every list endpoint is cursor-paginated, so paging stays correct and fast even when the list is changing underneath you.
Every list endpoint returns the same envelope:
{
"object": "list",
"url": "/v1/contacts",
"has_more": true,
"data": [/* … */]
}
Four fields, and that is the whole shape. url is the path the page came from, has_more says
whether another page exists in the direction you are walking, and data holds the objects. There is
deliberately no total and no page count, because both cost a full scan.
Paging forward#
Pass the last id you saw as starting_after:
curl "https://api.getlaunchlist.com/v1/contacts?project=prj_7Qk2mN4pR8&limit=100" \
-H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk"
curl "https://api.getlaunchlist.com/v1/contacts?project=prj_7Qk2mN4pR8&limit=100&starting_after=con_4mKp9Wq2Rx" \
-H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk"
Here con_4mKp9Wq2Rx is the id of the last contact in the first page's data. Keep going while
has_more is true. ending_before pages the other way, and every page reads newest first either
way.
| Parameter | Default | Accepts |
|---|---|---|
limit |
25 | 1 to 100 |
starting_after |
none | an object id, to page toward older records |
ending_before |
none | an object id, to page toward newer records |
These are validated rather than quietly corrected, so a paging bug surfaces as an error instead of
as wrong data. A limit outside 1 to 100 is a 400, sending both starting_after and
ending_before is a 400, and an id that does not resolve to an object in this list is a 400 too.
Why cursors and not page numbers#
Offset pagination breaks while you page. Ask for page 2 after three new signups have arrived and you will re-read rows you already saw; if rows were deleted, you skip some. On a waitlist that is actively collecting signups, this is not a rare edge. It is the normal case.
Offset gets slower the deeper you go. OFFSET 50000 means the database counts fifty thousand
rows before returning anything. A cursor is an indexed seek, so page 500 costs the same as page 1.
At a million contacts, that difference is the whole design.
A complete loop#
const all = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
const url = new URL('https://api.getlaunchlist.com/v1/contacts');
url.searchParams.set('project', 'prj_7Qk2mN4pR8');
url.searchParams.set('limit', '100');
if (cursor) {
url.searchParams.set('starting_after', cursor);
}
const page = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.LAUNCHLIST_KEY}` },
}).then((response) => response.json());
all.push(...page.data);
// The cursor is the last id on this page. The response does not carry one.
cursor = page.data.at(-1)?.id ?? null;
hasMore = page.has_more && cursor !== null;
}
Next#
- List contacts
- Rate limits: pace a full sync
Common questions
How do I page through all my contacts via the API?
Request a page, then pass the last item's id as starting_after to get the next one. Repeat while has_more is true. There are no page numbers and no offset.
Why does the LaunchList API not use page numbers?
Offset pagination skips and repeats rows when the list changes while you are paging, and it gets slower the deeper you go. Cursors are stable under concurrent writes and stay fast at any depth.
How many results can I get per request?
Up to 100, with a default of 25. Ask for what you will use: a larger page is one round trip, but a slower one.