# Pagination

List endpoints return their results one page at a time, newest first, using **keyset pagination**: you ask for the page that comes after the last item you have. Unlike page numbers, this stays correct while new items keep arriving. This page explains the parameters, the response fields and how to walk a whole list.

## Parameters

| Parameter | Meaning |
|---|---|
| `limit` | How many items to return. The default and the maximum depend on the list (see below). A value out of range fails with `422 validation_error`. |
| `after` | The `id` of the last item of the previous page. The response starts right after it. Leave it out for the first page. |

`GET /emails` also takes `before`, the `id` of an item, to fetch the items newer than it.

## Response

Every list answers with the same envelope:

```json
{
  "object": "list",
  "has_more": true,
  "next_cursor": "pay_01a0b904297873e8b3524a26fdca0ab5",
  "data": [
    { "object": "payment", "id": "pay_01a0b904...", "status": "paid" },
    { "object": "payment", "id": "pay_01a0b903...", "status": "pending" }
  ]
}
```

| Field | Meaning |
|---|---|
| `data` | The items of this page |
| `has_more` | `true` when there is at least one more page |
| `next_cursor` | Payments lists only: the value to pass as `after` for the next page, or `null` on the last page. It is the `id` of the last item in `data`. |

On lists without `next_cursor`, pass the `id` of the last item in `data` as `after`. The result is the same.

## Limits per list

| Lists | Default `limit` | Maximum |
|---|---|---|
| Email: emails, received emails, contacts, templates, logs | 20 | 100 |
| Payments: payments, transactions, e-invoices | 20 | 100 |
| Support: conversations | 50 | 100 |
| Support, Products and CRM: tickets, products, orders, customers | 50 | 200 |

A few small lists, such as domains, webhooks and broadcasts, return every item in one response.

Conversation messages page differently, by sequence number: `GET /chat/conversations/{id}/messages` takes `after_seq` (the `seq` of the last message you have) and a `limit` of up to 200, 100 by default. That is how you sync a conversation incrementally.

## Walk through a whole list

Fetch a page, process it, and keep going while `has_more` is `true`:

**cURL**

```bash
# First page
curl "https://api.hatang.dev/payments?limit=100" \
  -H "Authorization: Bearer $HATANG_API_KEY"

# Next page: pass next_cursor (or the last id) as after
curl "https://api.hatang.dev/payments?limit=100&after=pay_01a0b904297873e8b3524a26fdca0ab5" \
  -H "Authorization: Bearer $HATANG_API_KEY"
```

**Node.js**

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });

async function* allEmails() {
  let after;
  while (true) {
    const { data: page, error } = await hatang.emails.list({ limit: 100, after });
    if (error) throw new Error(`${error.name}: ${error.message}`);
    yield* page.data;
    if (!page.has_more || page.data.length === 0) return;
    after = page.data[page.data.length - 1].id;
  }
}

for await (const email of allEmails()) {
  console.log(email.id, email.last_event);
}
```

**Python**

```python
from hatang import Hatang

hatang = Hatang()  # reads HATANG_API_KEY

def all_payments():
    after = None
    while True:
        page = hatang.payments.list(limit=100, after=after)
        yield from page["data"]
        if not page["has_more"]:
            return
        after = page.get("next_cursor") or page["data"][-1]["id"]

for payment in all_payments():
    print(payment["id"], payment["status"])
```

**PHP**

```php
function allConversations(GuzzleHttp\Client $client): Generator {
    $query = ['limit' => 100];
    do {
        $page = json_decode((string) $client->get('/chat/conversations', ['query' => $query])->getBody(), true);
        yield from $page['data'];
        $last = end($page['data']);
        $query['after'] = $last ? $last['id'] : null;
    } while ($page['has_more'] && $last);
}

foreach (allConversations($client) as $conversation) {
    echo $conversation['id'], PHP_EOL;
}
```

> **Tip**
>
> Filters such as `status` stay the same on every page: send them with each request together with `after`. Each page is one request against the [rate limit](https://hatang.dev/docs/guides/getting-started/rate-limits.md), so use the largest `limit` you can handle.

## Next steps

- [Rate limits](https://hatang.dev/docs/guides/getting-started/rate-limits.md)
- [Errors](https://hatang.dev/docs/guides/getting-started/errors.md)
- [REST API reference](https://hatang.dev/docs/reference/api.md)

Source: https://hatang.dev/docs/guides/getting-started/pagination
