Skip to content

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 ​

EndpointPaginatedsearch
GET /api/v1/campaignsYesAccepted, no effect
GET /api/v1/recipientsYesFilters on name
GET /api/v1/recipients/by-list/:listIdYesFilters on name
GET /api/v1/templatesYesFilters on name
GET /api/v1/listsNo — returns every list—

Parameters ​

ParameterTypeDefaultNotes
cursoruuid—An id from a previous page's meta
limitinteger201–100. Outside that range is a 400
directionnext | prevnextWhich way to page
searchstring—≤200 characters, case-insensitive substring on name

Response ​

json
{
  "data": [ /* newest first */ ],
  "meta": { "count": 250, "nextCursor": "550e8400-...", "prevCursor": null }
}
FieldMeaning
countTotal matching rows, not the page size. Respects search
nextCursorPass as cursor for the following page. null = no more
prevCursorPass 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=prev

Without 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 ​

StatuscodeWhen
400validation_failedlimit 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.