Skip to content
RelationsAI
API fundamentals

Pagination

List operations return results in pages. This page explains the limit and cursor parameters, the list envelope and how cursors behave.

Parameters

ParameterTypeMeaning
limitinteger, 1 to 100Most objects to return. Default 25. Outside the range: validation_error.
cursorstringThe next_cursor of the previous page, exactly as returned. Omit it for the first page.

Every list response has the same envelope:

JSONJSON
{ "object": "list", "data": [ { "id": "call_9Tg5Yh2Uj8Ik4Ol7Pm3Nb6", "object": "call", "livemode": false, "status": "ended" }, { "id": "call_6Vb3Nm8Qw2Er5Ty9Ui4Op7", "object": "call", "livemode": false, "status": "queued" } ], "has_more": true, "next_cursor": "eyJ2IjoxLCJrIjpbIjIwMjYtMTAtMjFUMDk6MzA6MDAuMDAwWiJdfQ.c2lnbmF0dXJl" }
FieldMeaning
dataThe objects on this page (full objects, as from the single-object read).
has_moretrue when more objects exist after this page.
next_cursorPass as cursor to get the next page. null on the last page.

(Objects in this example are shortened, and the cursor value is illustrative.)

Order

Most lists are newest first: by created_at descending, then by id. A few lists have a different, stable order:

Objects created while you page through a newest-first list appear at the start, so they are not on later pages: start a new listing (or use created_after) to pick them up.

Iterating

Request pages until has_more is false, passing each page's next_cursor as cursor and keeping every filter the same. A page can hold fewer than limit objects while has_more is still true (this happens when you filter calls by status), so always use has_more, never the page size, to decide when to stop.

The first page (here one call per page, to keep the example short):

curl "https://voice-api.relationsai.com/v1/calls?limit=1" \ -H "Authorization: Bearer <token>"
JSONJSON
{ "object": "list", "data": [ { "id": "call_9Tg5Yh2Uj8Ik4Ol7Pm3Nb6", "object": "call", "livemode": false, "status": "ended", "outcome": "no_answer", "end_reason": "no_answer", "direction": "outbound", "source": "api", "agent_id": "agent_1ZltiWzxrXmMprGD6q63", "campaign_id": null, "phone_number_id": "pn_6Vhm3sFrB5UpmXAnsvmDu", "from": "+12025550150", "to": "+12025550101", "metadata": {}, "created_at": "2026-10-21T09:35:00.000Z", "ended_at": "2026-10-21T09:35:18.000Z", "duration_seconds": 0, "billable_minutes": 0, "cost": { "amount": 0, "currency": "INR" }, "artifacts": { "transcript": { "state": "absent" }, "recording": { "state": "absent" }, "analysis": { "state": "absent" } }, "settled": true, "settled_at": "2026-10-21T09:35:30.000Z" } ], "has_more": true, "next_cursor": "eyJ2IjoxLCJrIjpbIjIwMjYtMTAtMjFUMDk6MzA6MDAuMDAwWiIsImNhbGxfNlZiM05tOFF3MkVyNVR5OVVpNE9wNyJdfQ.Qm9vbGVhbk1hYw" }

has_more is true, so request the next page with cursor set to next_cursor, exactly as returned, and the same filters (limit may change):

curl "https://voice-api.relationsai.com/v1/calls?limit=1&cursor=eyJ2IjoxLCJrIjpbIjIwMjYtMTAtMjFUMDk6MzA6MDAuMDAwWiIsImNhbGxfNlZiM05tOFF3MkVyNVR5OVVpNE9wNyJdfQ.Qm9vbGVhbk1hYw" \ -H "Authorization: Bearer <token>"
JSONJSON
{ "object": "list", "data": [ { "id": "call_6Vb3Nm8Qw2Er5Ty9Ui4Op7", "object": "call", "livemode": false, "status": "queued", "outcome": null, "end_reason": null, "direction": "outbound", "source": "api", "agent_id": "agent_1ZltiWzxrXmMprGD6q63", "campaign_id": null, "phone_number_id": "pn_6Vhm3sFrB5UpmXAnsvmDu", "from": "+12025550150", "to": "+12025550100", "metadata": { "crm_lead_id": "L-20931" }, "created_at": "2026-10-21T09:30:00.000Z", "ended_at": null, "duration_seconds": null, "billable_minutes": null, "cost": null, "artifacts": { "transcript": { "state": "pending" }, "recording": { "state": "pending" }, "analysis": { "state": "pending" } }, "settled": false, "settled_at": null } ], "has_more": false, "next_cursor": null }

has_more is false and next_cursor is null: this was the last page. (The cursor in these examples is illustrative; real cursors are opaque.)

Each page request counts against the read rate limit. Use limit=100 and filters to read fewer pages.

The complete runnable example reads every page of ended calls from the last 24 hours, with the same filters on every page, and counts them by outcome.

Complete runnable example

Read every page of a list: follow next_cursor, with the same filters, until has_more is false.

The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.

Cursor rules

  • Cursors are opaque: do not decode, edit, build or shorten them. They are URL-safe strings of up to 512 characters.
  • A cursor is bound to the list and the filters that produced it, and to your workspace and mode. Send it only to the same list (for recipients and deliveries, the same campaign or endpoint id in the path) with exactly the same filters; limit may change between pages.
  • A cursor that was modified or is sent with different filters answers invalid_cursor (400, retry: no); one longer than 512 characters answers validation_error. Start again without a cursor.
  • Use a cursor to continue the listing you are reading now, not as a long-lived bookmark. To resume a sync later, store the created_at of the newest object you processed and list again with created_after.

To change filters, start a new listing from the first page.

Errors

CodeCauseFix
invalid_cursorEdited or truncated cursor, a cursor from another list, or filters changed between pagesSend next_cursor exactly as returned with the same filters, or start again without a cursor.
validation_errorlimit below 1 or above 100, an unknown query parameter, or a parameter given twiceFix the query string. details.errors names the parameter.