Skip to content

Pagination

How list endpoints page through results, what the response looks like, how to walk every page safely, and the endpoints that behave differently.

How it works#

Paginated list endpoints use page numbers. You ask for a page and a page size; the response returns that slice plus the totals you need to know when to stop.

pageintegerdefault 1
Which page to return, starting at 1.
per_pageintegerdefault 20
Items per page, from 1 to 100.

Both are query parameters and combine with each endpoint's own filters:

bash
curl "https://api.poststack.dev/emails?page=2&per_page=50&status=bounced" \
  -H "Authorization: Bearer $POSTSTACK_API_KEY"

The response puts the items in data and the paging state in meta:

json
{
  "data": [
    { "id": 48213, "publicId": "em_k3v9x2m8q1w7r4t6y0p5n2bz", "status": "bounced", "…": "…" },
    { "id": 48190, "publicId": "em_c8n1q4w0z7x2v5b9m3k6j1hd", "status": "bounced", "…": "…" }
  ],
  "meta": {
    "page": 2,
    "perPage": 50,
    "total": 137,
    "totalPages": 3
  }
}
dataobject[]
This page's items, in the endpoint's order (see the table below).
meta.pageinteger
The page you got.
meta.perPageinteger
The page size used.
meta.totalinteger
Items matching your filters across all pages.
meta.totalPagesinteger
ceil(total / perPage) — 0 when nothing matches.

snake_case in, camelCase out

You send per_page, but meta reports perPage and totalPages. The items themselves are also camelCase (publicId, createdAt).

Walking every page#

Request page 1, then keep going until page reaches meta.totalPages. Use per_page=100 to make the fewest requests. None of the SDKs has an auto-paging iterator, so the loop is yours:

import { PostStack } from '@poststack.dev/sdk';

const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);

const seen = new Set<string>();
for (let page = 1; ; page++) {
  const { data, meta } = await poststack.emails.list({ page, per_page: 100, status: 'bounced' });
  for (const email of data) {
    if (seen.has(email.publicId)) continue; // a new row can shift an item onto the next page
    seen.add(email.publicId);
    // ...handle email
  }
  if (page >= meta.totalPages) break;
}

Pages are computed at request time with an offset, not held open for you. If an item is created or deleted while you are paging, everything after it shifts by one: on a newest-first list, a new email arriving between page 1 and page 2 pushes the last item of page 1 onto page 2, so you see it twice. De-duplicate on the ID as above. For GET /emails you can also fix the window with date_to set to the time you started, so new sends can't enter the result.

Limits and errors#

  • Out-of-range values are rejected, not clamped. per_page=500, per_page=0, page=0 or a non-number answer 400, naming the parameter.
  • A page past the end is not an error. It returns 200 with an empty data and the real meta.total.
  • cursor does nothing. Some endpoints accept a cursor parameter without complaint, but it is ignored — you get page 1 (or whatever page says). Don't build on it.
  • List reads aren't rate-limited per endpoint, but they are subject to the per-IP edge limit of 30 requests a second.
bash
HTTP/1.1 400 Bad Request

{
  "error": "per_page: Too big: expected number to be <=100"
}

Paginated endpoints#

These return { data, meta } as described above, unless the table says otherwise.

EndpointParametersOrderNotes
GET /emailspage, per_page (20, max 100)Newest firstFilters: status, domain_id, to, from, subject, tag, date_from, date_to, provider, country, device. Bodies and attachments are left out of list items.
GET /contactspage, per_page (20, max 100)Newest first; oldest first when segment_id is setFilters: search, segment_id, engagement_segment, unsubscribed.
GET /segments/:id/memberspage, per_page (20, max 100)Oldest first404 if the segment doesn't exist.
GET /broadcastspage, per_page (20, max 100)Newest firstFilters: search, status.
GET /templatespage, per_page (20, max 100)Most recently updated firstFilters: search, published. Adds counts: { published, draft } beside meta.
GET /suppressionspage, per_page (20, max 100)Not guaranteedNo sort order, so pages can overlap or skip entries. For a complete list use GET /suppressions/export (CSV).
GET /inboundpage, per_page (20, max 100)Newest firstFilter: domain (a domain name). An unknown domain returns an empty page, not an error.
GET /subscription-topics/:id/subscriberspage, per_page (50, max 100)Most recently changed firstsubscribed=true|false picks subscribed or unsubscribed contacts; subscribed by default.
GET /mailboxespage, per_page (20, max 100)Newest firstFilter: domain_id. Deleted mailboxes are left out.
GET /mailboxes/aliasespage, per_page (20, max 100)Newest first—
GET /signup-formspage, per_page (20, max 100)Newest first—
GET /webhooks/:id/deliveriespage, per_page (20, max 100)Newest firstReturns { deliveries, pagination } instead of { data, meta }; pagination has the same four fields.
GET /notification-channels/:id/deliveriespage, per_page (20, max 100)Newest firstReturns { deliveries, meta }.
GET /domains/:id/dmarc/reportspage, perPage (20, max 100)Newest firstThe size parameter is camelCase here and per_page is ignored. Values over 100 are clamped rather than rejected. Returns { reports, pagination }.

SDK types for webhook deliveries and DMARC reports

The TypeScript and Go SDKs type webhooks.getDeliveries and domains.getDmarcReports as { data, meta }, but the API returns the envelopes in the table above — read deliveries or reports and pagination from the result. The SDKs also send per_page, which the DMARC endpoint ignores, so those pages are always 20 long.

Lists that aren't paginated#

These return every item in one response, under a key named after the resource. They take no page or per_page; any you send are ignored.

EndpointResponseOrder
GET /domains{ domains: [...] }Newest first
GET /api-keys{ keys: [...] }Oldest first
GET /segments{ segments: [...] }Oldest first
GET /segments/:id/broadcasts{ broadcasts: [...] }Newest first
GET /webhooks{ webhooks: [...] }Newest first
GET /notification-channels{ channels: [...] }Newest first
GET /subscription-topics{ topics: [...] }Newest first
GET /subscription-topics/contacts/:contactId/subscriptions{ subscriptions: [...] }Not guaranteed
GET /workflows{ data: [...] } with no metaOldest first
GET /contact-properties{ properties: [...] }By name, A–Z
GET /templates/presets{ data: [...] }Fixed
GET /emails/:id/events{ events: [...] }Oldest first
GET /inbound/:id/attachments{ attachments: [...] }Not guaranteed
GET /broadcasts/:id/variants{ variants: [...] }Not guaranteed
GET /mailboxes/:id/shares{ shares: [...] }Not guaranteed
GET /mailboxes/:id/filters{ rules: [...] }Rule position

Lists capped by a limit parameter

Three broadcast reports take limit instead of pages. There is no way to get past the maximum.

EndpointlimitResponse
GET /broadcasts/performanceDefault 20, max 100{ metric, since, broadcasts: [...] }
GET /broadcasts/:id/non-openersDefault 500, max 5,000{ broadcast, count, contacts: [...] }
GET /broadcasts/:id/non-clickersDefault 500, max 5,000{ broadcast, count, contacts: [...] }

Troubleshooting#

I get the same items on every page

Check that you are sending page, not only cursor — cursor is ignored, so every request returns page 1. On DMARC reports, the size parameter is perPage.

My export is missing a few suppressions

GET /suppressions has no defined order, so offset pages can overlap. Download GET /suppressions/export instead, or de-duplicate and re-check the total against meta.total.

An item appeared on two pages

Something was created while you were paging and shifted the offsets. De-duplicate on the ID, or pin the window with a date filter where the endpoint has one.

Next steps

Was this page helpful?

Related