Pagination
Cursor pagination on every list endpoint: the limit bounds, the meta block, next_cursor and has_more, and why a cursor never skips or repeats a row.
Cursors, not offsets
Every list endpoint returns newest first and pages with an opaque cursor. Pass the previous page's next_cursor as ?cursor= to get the next page; when it is null you have everything.
The cursor encodes the position of the last row you saw, ordered by creation time with the id as a tiebreak. Because that pair is immutable and unique, a walk through a list is stable under concurrent inserts: a record created while you page appears at the front of a fresh walk, and a row you have seen is never repeated or skipped. Offset pagination cannot promise that, which is why it is not offered.
400 bad_request.Page size
?limit= is 20 by default and at most 100. A value outside 1–100 returns 400 validation_error rather than being silently clamped — a clamped 1000 would hide a client bug behind a page that looks right.
The meta block
A list response is { data: [...], meta: { ... } }. The three fields of meta:
| Name | Type | Description |
|---|---|---|
next_cursorrequired | string | null | Pass as `?cursor=` to fetch the next page; null on the last page |
has_morerequired | boolean | Whether another page exists |
limitrequired | integer | The page size that was applied |
has_more is known without a count query — the server fetches one row past the limit — so it is cheap and always accurate for the moment the page was built.
Example
GET /contacts with a page size and a search filter:
curl "https://your-crm.example.com/api/v1/contacts?limit=20&q=ayesha" \
-H "Authorization: Bearer $RAABTA_API_KEY"{
"data": [
{
"id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
"name": "Ayesha Khan",
"phone": "+971501234567",
"email": "ayesha@example.com",
"company": "Khan Trading LLC",
"avatar_url": null,
"tags": [
{
"id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"name": "VIP",
"color": "#10b981"
}
],
"custom_fields": [
{
"field_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"name": "Source",
"value": "Instagram ad"
}
],
"created_at": "2026-09-01T08:15:00.000Z",
"updated_at": "2026-09-02T10:00:00.000Z"
}
],
"meta": {
"next_cursor": null,
"has_more": false,
"limit": 20
}
}Walking every page:
const url = new URL("https://your-crm.example.com/api/v1/contacts");
url.searchParams.set("limit", "100");
let cursor = null;
do {
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.RAABTA_API_KEY}` },
});
const { data, meta } = await res.json();
for (const row of data) handle(row);
cursor = meta.next_cursor;
} while (cursor);Filters (q, status, updated_since, …) are passed alongside the cursor on every page. Changing a filter mid-walk invalidates the cursor; start again from the first page.
Paginated endpoints
Every endpoint that returns a page. The rest return one object.
- GET
/api/v1/contacts— List contacts - GET
/api/v1/contacts/{id}/notes— List notes on a contact - GET
/api/v1/inquiries— List inquiries - GET
/api/v1/inquiries/{id}/notes— List notes on an inquiry - GET
/api/v1/conversations— List conversations - GET
/api/v1/conversations/{id}/messages— List messages in a conversation - GET
/api/v1/broadcasts— List broadcasts - GET
/api/v1/broadcasts/{id}/recipients— List a broadcast’s recipients - GET
/api/v1/deals— List deals - GET
/api/v1/assistants— List assistants - GET
/api/v1/instructions— List the instruction library - GET
/api/v1/memories— List assistant memories - GET
/api/v1/triggers— List triggers - GET
/api/v1/runs— List runs - GET
/api/v1/runs/{id}/events— List a run’s events - GET
/api/v1/asks— List human approvals - GET
/api/v1/webhooks/{id}/deliveries— List deliveries
- Lists are ordered by
created_atdescending, always. Conversations are not ordered by last activity, because that moves; useupdated_sinceto find recent activity.