API RU
Early access. API keys are issued on request. How to get a key

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:

JSON
{
  "object": "list",
  "data": [ { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31" } ],
  "has_more": true,
  "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU"
}
FieldDescription
objectAlways list.
dataThe objects on the current page.
has_moretrue if more data follows this page.
next_cursorThe cursor for the next page. null when there are no more pages.
metaAdditional 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: the next_cursor value 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

  1. Send the first request with the filters you need.
  2. Process data.
  3. If has_more is true, repeat the request with the same filters and cursor=<next_cursor>.
  4. Stop when has_more is false.
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 });

#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 limit between 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 on has_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.