Cursor-Based Pagination
List endpoints page through results using opaque cursors. Reliable across inserts, no offset drift.
Every list endpoint in the Macha API is cursor-paginated. You walk a list by passing the previous response's next_cursor back as a query parameter. Cursors are opaque strings, treat them as bytes, don't try to parse them.
Why cursors, not offsets
Offset pagination (?page=3&per_page=50) is what you've seen in older APIs. It breaks under two conditions Macha cares about:
- Inserts during traversal. If a new conversation lands while you're paging, every subsequent page either skips a record or duplicates one.
- Deep pagination.
OFFSET 50000means the database has to count past 50,000 rows. Slow.
Cursor pagination uses the last-seen record's ID as the boundary, so each page query is "give me 50 things older than conv_X." This is fast (indexed) and stable under concurrent inserts.
Request
List endpoints accept two query params:
| Param | Type | Default | Range |
|---|---|---|---|
cursor | string | none (start at newest) | opaque |
limit | integer | 25 | 1–100 |
The first request omits cursor:
curl 'https://dashboard.getmacha.com/api/v1/conversations?limit=50' \
-H "Authorization: Bearer $MACHA_API_KEY"
The server returns up to 50 items, sorted newest-first, plus a next_cursor if there's more:
{
"data": [ /* ...up to 50 conversations... */ ],
"meta": {
"request_id": "req_...",
"next_cursor": "conv_6a28f567ba502f55d177bb50"
}
}
Pass that cursor back for the next page:
curl 'https://dashboard.getmacha.com/api/v1/conversations?limit=50&cursor=conv_6a28f567ba502f55d177bb50' \
-H "Authorization: Bearer $MACHA_API_KEY"
When there's no next page, next_cursor is null:
{
"data": [ /* ...last batch of items... */ ],
"meta": {
"request_id": "req_...",
"next_cursor": null
}
}
Walking the full list
The standard loop is straightforward:
async function listAll(resource) {
const all = [];
let cursor = null;
while (true) {
const params = new URLSearchParams({ limit: '100' });
if (cursor) params.set('cursor', cursor);
const res = await fetch(
`https://dashboard.getmacha.com/api/v1/${resource}?${params}`,
{ headers: { Authorization: `Bearer ${process.env.MACHA_API_KEY}` } },
);
const body = await res.json();
all.push(...body.data);
if (!body.meta.next_cursor) break;
cursor = body.meta.next_cursor;
}
return all;
}
const allAgents = await listAll('agents');
Python equivalent
import os, requests
def list_all(resource):
out, cursor = [], None
while True:
params = {'limit': 100}
if cursor: params['cursor'] = cursor
r = requests.get(
f'https://dashboard.getmacha.com/api/v1/{resource}',
headers={'Authorization': f'Bearer {os.environ["MACHA_API_KEY"]}'},
params=params,
)
body = r.json()
out.extend(body['data'])
cursor = body['meta'].get('next_cursor')
if not cursor: break
return out
Ordering
Default sort order is newest first, rows are returned in descending order of their internal ID, which corresponds to creation time. This is stable across paginated requests unless explicitly documented otherwise.
The exception is the conversation messages endpoint (GET /conversations/:id/messages), which returns messages in ascending order, oldest first, because that's the natural reading order for a chat thread.
Cursor opacity rules
Macha cursors today look like prefixed IDs (conv_..., agent_...). Don't write code that assumes this. Treat the cursor string as a black box:
Cursors may switch from prefixed IDs to base64-encoded tokens or other formats without a version bump. The contract is "round-trip this string verbatim", nothing more.
Things you can rely on:
- Cursors are URL-safe strings. No special encoding needed.
- Cursors are stable for the lifetime of the listed resource, pausing for an hour mid-traversal and resuming is fine.
- Passing an invalid cursor returns
422 validation_failed.
Limit caps
Server enforces 1 ≤ limit ≤ 100. Asking for more than 100 returns 422 validation_failed. Asking for 0 or negative returns the same.
If you need everything, set limit=100 and loop. Don't try to bypass the cap.
Counting
The Macha API does not return total counts on list endpoints, no total, no has_more. We don't compute the count because doing so requires scanning the full result set, which is precisely what cursor pagination is built to avoid.
If you need a count for display, walk the list once and count. For "page X of Y" UIs, redesign, cursor pagination doesn't have stable page numbers.
Filter + pagination
Filter params (e.g. ?source=autonomous, ?model=gpt-5-mini) compose freely with cursor and limit. The cursor is filter-aware: pulling page 2 with the same filters returns the next 50 matching items, not the next 50 items globally.
curl 'https://dashboard.getmacha.com/api/v1/conversations?source=autonomous&model=gpt-5-mini&limit=50' \
-H "Authorization: Bearer $MACHA_API_KEY"
# next page, same filters:
curl 'https://dashboard.getmacha.com/api/v1/conversations?source=autonomous&model=gpt-5-mini&limit=50&cursor=' \
-H "Authorization: Bearer $MACHA_API_KEY"
Don't change filters mid-traversal, the cursor was scoped to the original filter set. Restart from scratch (no cursor) when you change the filter.
© 2026 AGZ Technologies Private Limited