Appearance
Pagination
Collections that can grow are cursor-paginated. Cursors are ids, and ids are time-ordered UUIDv7 values, so paging stays stable while rows are added.
Which endpoints paginate
| Endpoint | Paginated | search |
|---|---|---|
GET /api/v1/campaigns | Yes | Accepted, no effect |
GET /api/v1/recipients | Yes | Filters on name |
GET /api/v1/recipients/by-list/:listId | Yes | Filters on name |
GET /api/v1/templates | Yes | Filters on name |
GET /api/v1/lists | No — returns every list | — |
Parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
cursor | uuid | — | An id from a previous page's meta |
limit | integer | 20 | 1–100. Outside that range is a 400 |
direction | next | prev | next | Which way to page |
search | string | — | ≤200 characters, case-insensitive substring on name |
Response
json
{
"data": [ /* newest first */ ],
"meta": { "count": 250, "nextCursor": "550e8400-...", "prevCursor": null }
}| Field | Meaning |
|---|---|
count | Total matching rows, not the page size. Respects search |
nextCursor | Pass as cursor for the following page. null = no more |
prevCursor | Pass as cursor with direction=prev. null = at the start |
Results are newest first, because ids sort chronologically.
Iterating
Stop when nextCursor is null.
bash
# First page
curl -H "x-api-key: $DRSE_API_KEY" \
"$DRSE_BASE_URL/api/v1/recipients?limit=100"
# Next page — use nextCursor from the previous meta
curl -H "x-api-key: $DRSE_API_KEY" \
"$DRSE_BASE_URL/api/v1/recipients?limit=100&cursor=550e8400-e29b-41d4-a716-446655440004"ts
async function* allRecipients(listId?: string) {
let cursor: string | null = null;
for (;;) {
const params = new URLSearchParams({ limit: "100" });
if (cursor) params.set("cursor", cursor);
const path = listId
? `/api/v1/recipients/by-list/${listId}`
: "/api/v1/recipients";
const res = await fetch(`${BASE}${path}?${params}`, {
headers: { "x-api-key": KEY },
});
if (!res.ok) {
const body = await res.json();
throw new Error(`${body.error.code}: ${body.error.message}`);
}
const { data, meta } = await res.json();
yield* data;
if (!meta.nextCursor) return;
cursor = meta.nextCursor;
}
}
for await (const recipient of allRecipients()) {
console.log(recipient.name);
}Mind the rate limit on large sweeps
At limit=100 and 100 requests per minute that is about 10,000 rows a minute. Bigger sweeps need pacing — see Rate limits.
Paging backwards
direction=prev with a cursor walks towards newer rows. Results always arrive newest-first regardless of direction, so a "previous page" reads like a first page.
?cursor=<prevCursor>&direction=prevWithout a cursor, direction=prev is the same as the first page.
Stability
Because cursors are ids rather than offsets, a row inserted mid-walk does not shift the rows you have yet to see — the duplicate-and-skip problem of ?page=2 does not arise. Paging with nextCursor gives a consistent walk from where you started.
Rows created after you began appear only in a fresh first page, since paging next moves towards older data.
count is a total
20 items with "count": 250 means 250 match your filter overall. Use count to display "250 recipients", never for loop control — loop on nextCursor.
search matches names only
For recipients, the name field only — not street, post town, country or custom fields. For templates, the template name.
Matching is case-insensitive substring, so alice finds "Alice Andersson" and ander finds it too. count reflects the filtered total.
search is silently inert on campaigns
GET /api/v1/campaigns accepts search without error and ignores it. You will get an unfiltered page with no warning. Filter campaigns client-side, or track your own send records.
Lists are not paginated
GET /api/v1/lists returns every list, with a different meta:
json
{ "meta": { "totalRecipientCount": 250, "listCount": 3 } }No cursors, because there are no pages. totalRecipientCount is the account-wide recipient count.
Errors
| Status | code | When |
|---|---|---|
| 400 | validation_failed | limit outside 1–100, cursor not a UUID, direction not next/prev, search over 200 characters |
An unknown-but-valid UUID cursor is not an error — it returns an empty page.