> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ibubbl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> How list endpoints page through results: newest first, a starting_after cursor, has_more, and the rules every list follows.

List endpoints return results a page at a time. Every paginated list in the iBubbl API works the same way, so one loop covers all of them. [List messages](https://docs.ibubbl.com/api-reference/messages/list.md) and [List contacts](https://docs.ibubbl.com/api-reference/contacts/list.md) follow these rules, and lists added later will too.

## How it works

1. Request the first page. Pass `limit` (1 to 100, default 50) and any filters.
2. The response is `{"data": [...], "has_more": true | false}`.
3. If `has_more` is `true`, request the next page with the same filters plus `starting_after` set to the `id` of the **last** item in `data`.
4. Stop when `has_more` is `false`.

```json theme={null}
{
  "data": [
    { "id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c", "created_at": "2026-11-10T15:06:41.518204Z", "...": "..." },
    { "id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b", "created_at": "2026-11-10T15:04:10.112730Z", "...": "..." }
  ],
  "has_more": true
}
```

The next request adds `starting_after=8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b`.

## The rules

* **Newest first.** Items are ordered by `created_at` descending, then by `id` descending for items created in the same microsecond. The order is total: two requests never order the same items differently.
* **The cursor is an ID.** `starting_after` is the `id` of an item you already have, not a page number or an opaque token. You can store it, log it and read it. It stays valid if you change filters: the next page starts after that item's position, whether or not the item itself matches the new filters.
* **Pages don't shift.** Each page starts from the cursor item's position, not from a count of rows. Items created while you page through can't push an item onto two pages or make one disappear. New items appear at the front of the list, so a loop that started before they arrived doesn't see them; run it again with `created_after` to pick them up.
* **`has_more` is exact.** When it's `false` there is nothing after the last item. A page can be shorter than `limit` only when it's the last one, and an empty `data` always comes with `has_more: false`.
* **`limit` is strict.** Outside 1–100 is `422 validation_error`, not rounded to the nearest valid value.
* **Unknown parameters are refused.** A query parameter the endpoint doesn't know, or one given twice, returns `422 validation_error` naming it. This is stricter than request bodies on purpose: a misspelt cursor such as `startingAfter`, quietly ignored, would return the first page forever and your loop would never end.
* **An unknown cursor is an error.** A `starting_after` that isn't one of your items' IDs returns `422 invalid_cursor`. Another account's ID is answered the same way, and so is a contact deleted after you received it: start again from the first page.
* **Time filters are exclusive and need a time zone** (on lists that have them, such as List messages). `created_after` and `created_before` exclude the exact time given. Write times as ISO 8601 with `Z` (`2026-11-10T09:30:00Z`); `created_at` values come back in that form, so you can pass them back unchanged. In a URL, an unencoded `+` (as in `+00:00`) reads as a space, so encode it as `%2B` or use `Z`.
* **Filters are applied before paging.** A rare filter value still returns every match across pages. A filter that matches nothing returns an empty page, not `404`.

## Read every page

The loop below fetches page after page until `has_more` is `false`, and yields items one at a time. Pass any filters the endpoint takes. If it goes over the [list rate limit](https://docs.ibubbl.com/concepts/limits.md#rate-limits), it waits as long as `Retry-After` says and asks for the same page again.

```typescript TypeScript theme={null}
type Message = {
  id: string;
  direction: "inbound" | "outbound";
  created_at: string;
  status: string;
  body: string;
  contact_id: string | null;
  conversation_id: string | null;
};

async function* listMessages(
  filters: Record<string, string> = {},
): AsyncGenerator<Message> {
  let startingAfter: string | undefined;
  while (true) {
    const params = new URLSearchParams({ limit: "100", ...filters });
    if (startingAfter) params.set("starting_after", startingAfter);

    const res = await fetch(`https://api.ibubbl.com/v1/messages?${params}`, {
      headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    if (res.status === 429) {
      // Over the list rate limit: wait, then ask for the same page again.
      const seconds = Number(res.headers.get("Retry-After") ?? "1");
      await res.body?.cancel();
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    const data = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);

    yield* data.data as Message[];
    if (!data.has_more) return;
    startingAfter = data.data[data.data.length - 1].id;
  }
}

for await (const message of listMessages({ direction: "inbound" })) {
  console.log(message.created_at, message.id, message.body);
}
```

```python Python theme={null}
import os
import time

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]


def list_messages(**filters):
    params = {"limit": 100, **filters}
    while True:
        res = requests.get(
            "https://api.ibubbl.com/v1/messages",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=params,
            timeout=10,
        )
        if res.status_code == 429:
            # Over the list rate limit: wait, then ask for the same page again.
            time.sleep(int(res.headers.get("Retry-After", "1")))
            continue
        data = res.json()
        if not res.ok:
            raise RuntimeError(f"{res.status_code} {data['detail']['error']}")

        yield from data["data"]
        if not data["has_more"]:
            return
        params["starting_after"] = data["data"][-1]["id"]


for message in list_messages(direction="inbound"):
    print(message["created_at"], message["id"], message["body"])
```

A few notes:

* **Keep the filters the same on every page**, and change only `starting_after`. The loops above do this by building each request from the same `filters`.
* **Fetch pages one at a time.** Each page needs the last ID of the one before it, so pages can't be fetched in parallel.
* **A list request is safe to retry.** On a network error, a timeout or a `5xx`, request the same page again with the same `starting_after`. Listing never changes anything. Don't retry a `4xx`: fix the request. The one exception is `429 rate_limited`: your account made more than 120 list requests in 60 seconds. Wait the number of seconds in the `Retry-After` header, then send the same request again. The refused request didn't count, and the cursor is still valid. See [Rate limits](https://docs.ibubbl.com/concepts/limits.md#rate-limits).
* **One budget per account.** Every API key on your account and every list endpoint share the 120 requests a minute. Two loops running side by side each get about half.
* **Big ranges.** Narrow the range with `created_after` / `created_before` rather than reading your whole history on every run. The [reconciliation recipe](https://docs.ibubbl.com/guides/webhook-retries.md#recover-from-downtime) shows how to keep a checkpoint.

## Next

> [card] [List messages](https://docs.ibubbl.com/api-reference/messages/list.md)
>
> Every filter and the response shape.

> [card] [List contacts](https://docs.ibubbl.com/api-reference/contacts/list.md)
>
> Search, tag, opt-out and block filters.

> [card] [Recover from downtime](https://docs.ibubbl.com/guides/webhook-retries.md#recover-from-downtime)
>
> Catch up on what your webhook endpoint missed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.