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:
{
"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
- Node.js
- Python
- PHP
# 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"
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);
}
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"])
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;
}
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, so use the largest limit you can handle.