Pagination and versioning
How to page through list endpoints with cursors, what the meta block tells you, and how the API changes over time.
Cursors, not page numbers
List endpoints page with an opaque cursor. Read one page, then pass its next_cursor back verbatim to get the next one.
# First page
curl -s "https://app.sourceloop.ai/api/v1/contacts?website=acme.com&limit=50" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY"
# Next page
curl -s "https://app.sourceloop.ai/api/v1/contacts?website=acme.com&limit=50&cursor=eyJ0IjoiMjAyNi0wOC0xMSJ9" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY"
Stop when meta.has_more is false. next_cursor is absent on the last page, so a loop that checks for the cursor works just as well.
Never construct, decode or edit a cursor. It encodes sort position, not an offset, which is what stops rows shifting under you while you page through a ledger that is still being written to.
Limits
| Endpoint | Default | Maximum |
|---|---|---|
GET /v1/contacts | 50 | 200 |
GET /v1/changes | 100 | 500 |
Asking for more than the maximum is not an error; you get the maximum.
The meta block
Every list response carries a meta block that describes the page you just received:
{
"meta": {
"website_id": "wst_9a4f",
"window": { "from": "2026-07-18T00:00:00Z", "to": "2026-08-16T23:59:59Z" },
"row_count": 50,
"total_count": null,
"has_more": true,
"pii_included": false,
"pii_note": "Email, phone and name are masked. Add the pii:read scope to a key to receive them."
},
"next_cursor": "eyJ0IjoiMjAyNi0wOC0xMSJ9"
}
total_count is null unless you ask for it with total_count=true. Counting the whole filtered set costs considerably more than returning a page, so it is opt-in rather than free. When you do ask, the count respects your filters, so “70 of 366” describes the filtered set rather than everything in the workspace.
The window is the range the API actually resolved, which is the one to display. If you asked for period=last 30 days, this is what that meant in the workspace’s timezone.
Versioning
The version lives in the path: every endpoint is under /v1. A key, a scope and a request shape that work today keep working.
Write your client so it tolerates growth:
- Ignore fields you do not recognise. New fields are added to responses as the product grows, and a client that rejects unknown keys will break on a change that was meant to be harmless.
- Do not depend on field order in objects, or on the exact wording of a
detailornotestring. Match ontypein error documents instead, which is stable. - Do not parse cursors, as above.
Anything that would break a correct client, such as removing a field or changing what an existing one means, ships as a new version path rather than being changed underneath /v1.
Two older paths remain from before the resources were renamed: /v1/conversions behaves exactly like /v1/contacts, and /v1/accounts exactly like /v1/companies. They still work and return identical responses. New integrations should use the current names, which are the ones documented in this reference.