API conventions
Pagination
List endpoints return results in pages, newest first, and are navigated with a cursor.
The list object#
Every list endpoint returns the same envelope.
{
"object": "list",
"data": [
{
"id": "msg_2b1Xw9aQ3rT8yU0pL4kZ",
"object": "message",
"mode": "live",
"channel_id": "ch_7Hq2mN5vB8cX1zL0pK3j",
"channel_type": "whatsapp",
"direction": "outbound",
"to": "+971501234567",
"from": "+971800123456",
"type": "text",
"content": {
"text": {
"body": "Your code is 482910"
}
},
"status": "delivered",
"error": null,
"reference": "order-1042",
"metadata": {
"user_id": "u_17"
},
"billing": {
"source": "package",
"amount_micros": 0,
"package_grant_id": "grant_5tGh2Kp9LmQ4xW7nB1cD",
"refunded": false
},
"sender": null,
"contact_id": null,
"created_at": "2026-10-05T09:30:00.000Z",
"updated_at": "2026-10-05T09:30:02.871Z",
"sent_at": "2026-10-05T09:30:01.210Z",
"delivered_at": "2026-10-05T09:30:02.871Z",
"read_at": null,
"failed_at": null
}
],
"has_more": true,
"next_cursor": "msg_2b1Xw9aQ3rT8yU0pL4kZ"
}| Field | Description |
|---|---|
object | Always list. |
data | The objects of this page, newest first. |
has_more | true if more objects exist after this page. |
next_cursor | The ID of the last object in data. Pass it as starting_after to get the next page. null when has_more is false. |
Parameters#
| Parameter | Default | Description |
|---|---|---|
limit | 20 | Page size, from 1 to 100. A value outside the range fails with 400 parameter_invalid. |
starting_after | None | An object ID. The page starts with the object that follows it. Use the next_cursor of the previous page. |
curl "https://api.omnimessage.co/v1/messages?limit=50&starting_after=msg_2b1Xw9aQ3rT8yU0pL4kZ" \
-H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx"Iterate over all results#
Request a page, process data, and repeat with starting_after set to next_cursor until has_more is false. Keep the filters identical between pages.
async function* listMessages(filters = {}) {
let startingAfter;
do {
const params = new URLSearchParams({ limit: '100', ...filters });
if (startingAfter) params.set('starting_after', startingAfter);
const response = await fetch(`https://api.omnimessage.co/v1/messages?${params}`, {
headers: { Authorization: `Bearer ${process.env.OMNIMESSAGE_API_KEY}` },
});
const page = await response.json();
if (!response.ok) throw new Error(`${page.error.code}: ${page.error.message}`);
yield* page.data;
startingAfter = page.has_more ? page.next_cursor : undefined;
} while (startingAfter);
}
for await (const message of listMessages({ status: 'failed', created_after: '2026-10-01T00:00:00Z' })) {
console.log(message.id, message.error.code);
}Consistency#
- Cursors are stable. Objects created while you paginate appear before your first page and do not shift later pages, so you never see an object twice or skip one.
- To pick up new objects later, start again from the first page and stop when you reach an ID you have already processed, or filter messages with
created_after. - A cursor is just an object ID and does not expire. An ID that does not exist fails with
400 parameter_invalid.
Paginated endpoints#
| Endpoint | Operation |
|---|---|
GET /v1/messages | List messages |
GET /v1/channels | List channels |
GET /v1/webhook_endpoints | List webhook endpoints |
GET /v1/contacts | List contacts |
GET /v1/contact_lists | List contact lists |
GET /v1/segments | List segments |
GET /v1/campaigns | List campaigns |
GET /v1/campaigns/{id}/recipients | List the recipients of a campaign |
GET /v1/integration_sources | List integration sources |
GET /v1/automation_events | List event receipts |
GET /v1/events | List events |
GET /v1/messages/{id}/events and GET /v1/channels/{id}/templates use the same envelope but return everything in one page: has_more is always false.