# Pagination and versioning

How to page through list endpoints with cursors, what the meta block tells you, and how the API changes over time.

Source: https://sourceloop.ai/help/api/pagination/

---

## 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.

```bash
# 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:

```json
{
  "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 `detail` or `note` string. Match on `type` in 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.