# Walking every page

List endpoints page with three query parameters and return three fields, the same as AgentMail.

| Query parameter | Type | Default | Meaning |
| --- | --- | --- | --- |
| `limit` | integer | `50` | Items per page. Values above `500` are capped at `500`. A missing value, `0` or a negative number means `50`; decimals are rounded down. A non-number returns `400 limit must be a number`. |
| `page_token` | string | none | The `next_page_token` from the previous page. |
| `ascending` | boolean | `false` | `true` for oldest first. Most lists are newest first by default. |

## Response

```
{
  "count": 50,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "inboxes": [ ... ]
}
```

| Field | Meaning |
| --- | --- |
| `count` | Number of items in this page (not a total). |
| `limit` | The limit that was applied. |
| `next_page_token` | Present only when there is another page. Pass it as `page_token` to get it. |
| `truncated` | Present and `true` on searches that hit their scan cap (see below). |

The items are under a key named after the resource: `inboxes`, `messages`, `threads`, `drafts`, `webhooks`, `deliveries`, `domains`, `pods`, `api_keys`, `entries`, `suppressions`, `events`, `agent_registrations`.

## Walking every page

### list-all.ts

```
async function listAll(path: string, key: string) {
  const items = [];
  let pageToken: string | undefined;
  do {
    const url = new URL(`https://api.machinemail.dev${path}`);
    url.searchParams.set('limit', '500');
    if (pageToken) url.searchParams.set('page_token', pageToken);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}` } });
    const page = await res.json();
    items.push(...page[key]);
    pageToken = page.next_page_token;
  } while (pageToken);
  return items;
}

const inboxes = await listAll('/v0/inboxes', 'inboxes');
```

## Page tokens

Treat tokens as opaque. Today a token encodes an offset into the result set, so:

- Items created while you page can shift later pages: an item can appear twice or be skipped. For a stable feed of changes, read the [event log](/content/docs/reference/api/events/index.html) with `after=<event_id>` instead.
- A token that cannot be decoded is ignored and the list starts again from the first page. It is not an error.
- Use a token with the same filters and sort order that produced it.

## Sort order

| List | Sorted by | Default |
| --- | --- | --- |
| Messages, threads | message or thread `timestamp` | newest first |
| Allow and block list entries | the `entry` value | descending (Z to A); `ascending=true` for A to Z |
| Everything else | `created_at` | newest first |
| Events with `after` | event order | oldest first |

## Endpoints that page differently

| Endpoint | Behaviour |
| --- | --- |
| `GET /v0/api-keys` | Returns up to `limit` non-revoked keys (from the newest 500). `limit` defaults to 50 and is not capped. Never returns `next_page_token`; `page_token` and `ascending` are ignored. |
| `GET /v0/webhooks` with a pod or inbox key | Returns every webhook in scope (up to 500) in one response, with no `next_page_token`. |
| `GET /v0/pods` with a pod key | Returns only that pod. |
| `GET /v0/inboxes/search` | Scans the newest 500 inboxes. Returns `count`, `inboxes` and `truncated: true` when there were more. No paging. |
| Thread search (`q` on thread lists, `/threads/search`) | Scans the newest 500 matching messages. `truncated: true` when it hit the cap. |
| `POST .../messages/batch-get`, `batch-update` | Not paged. At most 100 IDs; `limit` in the response is the number of IDs you sent. |
| `GET /v0/events` | `after` takes an event ID (`evt_...`) or an ISO 8601 time and switches the default to oldest first: a cursor you can poll. |
| `GET /v0/metrics/*` | Not paged. `limit` trims the result. |
| `GET /v0/threads/{thread_id}` | Not paged. Returns up to 500 messages. |
| MCP tools | `limit` is 1 to 100, default 20 (`list_inboxes` uses the API default of 50). |
