Pagination
Lists come back in pages. You move to the next page with a cursor, not with page numbers or offsets.
#List response
Every list uses the same envelope:
{
"object": "list",
"data": [ { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31" } ],
"has_more": true,
"next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU"
}| Field | Description |
|---|---|
object | Always list. |
data | The objects on the current page. |
has_more | true if more data follows this page. |
next_cursor | The cursor for the next page. null when there are no more pages. |
meta | Additional details. Present on conversations and the event feed. |
Lists don't return a total count (total) or an offset (offset).
#Parameters
limit: how many objects to return. Defaults to 50, from 1 to 100. The event feed allows up to 200.cursor: thenext_cursorvalue from the previous response.
Record order is fixed: oldest to newest by creation time, with ties broken by id. There is no sort parameter. New records are added to the end of the list.
#How to page through a list
- Send the first request with the filters you need.
- Process
data. - If
has_moreistrue, repeat the request with the same filters andcursor=<next_cursor>. - Stop when
has_moreisfalse.
async function listAll(path, params = {}) {
const items = [];
let cursor = null;
do {
const url = new URL(`https://api.melioralab.io/v1${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` },
});
if (!res.ok) throw new Error(`Meliora API: ${res.status} ${res.headers.get('Request-Id')}`);
const page = await res.json();
items.push(...page.data);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
return items;
}
const leads = await listAll('/leads', { campaign_id: '4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14', limit: 100 });import os
import requests
def list_all(path, params=None):
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['MELIORA_API_KEY']}"
params = dict(params or {})
items = []
while True:
resp = session.get(f"https://api.melioralab.io/v1{path}", params=params, timeout=15)
resp.raise_for_status()
page = resp.json()
items.extend(page["data"])
if not page["has_more"]:
return items
params["cursor"] = page["next_cursor"]
leads = list_all("/leads", {"campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "limit": 100})#Cursor rules
- Cursors are opaque. Don't parse or build them yourself. Just pass them back.
- A cursor is bound to its request. The filters on the next request must match the first one. If the filters differ, or the key's permissions or scope have changed, you get 400
cursor_mismatch. - You can change
limitbetween pages. That isn't an error. - A list cursor lasts 15 minutes. An expired cursor returns 410
cursor_expired: start again from the first page. An event feed cursor lasts 30 days. See Polling events. - A page can be shorter than
limit. A response never exceeds 1 MiB, so a long page is cut short and the remaining records come on the next page. Rely onhas_more, not on the record count.
#Lists aren't snapshots
You read a list page by page, and the data can change between requests: leads get added and statuses get updated. Records are never skipped or repeated because of shifting offsets, but a lead's status on the first page may have changed by the time you read the last one. If you need a consistent state, combine the list walk with the event feed: events after your anchor show what changed.