# Cursor pagination in the Mujib API with nextCursor

> How cursor pagination works in the Mujib REST API, with page size, result order, code that reads every page, and how to fetch only what is new since a sync.

The Mujib API uses cursor pagination: every list returns one page of results with a `nextCursor`, which you pass back as `cursor` on the next request until it comes back `null`. A page holds 50 items by default, and up to 100.

This applies to both lists, `GET /api/v1/contacts` and `GET /api/v1/bookings`. Single-item requests have no pages.

## How do pages work in the API?

Send the first request without `cursor`. The response carries the items in `data` and the next page's cursor in `nextCursor`:

```json
{
  "data": [ { "id": "5d8f2a61-0c4e-4b7a-9f13-6e2b8c0d4a57" }, { "id": "9b1e3c74-2a5d-4f08-8e6b-1c7d0a3f5e92" } ],
  "nextCursor": "MjAyNi0xMC0yOFQxNDowNToxMi40ODFafDliMWUzYzc0LTJhNWQtNGYwOC04ZTZiLTFjN2QwYTNmNWU5Mg"
}
```

(The items are cut down to their `id` here. Each request's page shows the full shape.)

| Parameter | Meaning |
| --- | --- |
| `limit` | Items per page, from one to 100. Without it, 50 |
| `cursor` | The previous page's `nextCursor`, exactly as received |

- When `nextCursor` is `null`, you have reached the last page.
- The last page may hold fewer items than `limit`, and `data` is empty when nothing matches.
- A cursor is opaque: do not decode it or build one yourself. A cursor that cannot be read returns a 400 with the message `Invalid cursor`.

## How do I read every page to the end?

Request a page, collect its items, request the next one with its cursor, and stop when the cursor is `null`. This function does that, and waits when it reaches a rate limit:

```js
const BASE = 'https://api.mujib.ai/api/v1';
const headers = { Authorization: `Bearer ${process.env.MUJIB_API_KEY}` };

async function fetchAll(path, params = {}) {
  const items = [];
  let cursor = null;
  for (;;) {
    const url = new URL(BASE + path);
    for (const [k, v] of Object.entries({ ...params, limit: 100 })) url.searchParams.set(k, String(v));
    if (cursor) url.searchParams.set('cursor', cursor);
    const res = await fetch(url, { headers });
    if (res.status === 429) {
      const wait = Number(res.headers.get('retry-after') ?? '60');
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    const body = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
    items.push(...body.data);
    if (body.nextCursor === null) return items;
    cursor = body.nextCursor;
  }
}

const bookings = await fetchAll('/bookings', { from: '2026-11-01', to: '2026-11-30' });
console.log(bookings.length);
```

```python
import os
import time
import requests

BASE = "https://api.mujib.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MUJIB_API_KEY']}"}

def fetch_all(path, params=None):
    items, cursor = [], None
    while True:
        query = {**(params or {}), "limit": 100}
        if cursor:
            query["cursor"] = cursor
        res = requests.get(BASE + path, params=query, headers=HEADERS, timeout=30)
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "60")))
            continue
        body = res.json()
        if not res.ok:
            raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")
        items.extend(body["data"])
        if body["nextCursor"] is None:
            return items
        cursor = body["nextCursor"]

contacts = fetch_all("/contacts", {"since": "2026-10-01T00:00:00Z"})
print(len(contacts))
```

## In what order do results come?

Each list has a fixed order that does not change between pages:

| List | Order |
| --- | --- |
| Contacts | Newest first, by `createdAt` (when the customer was first seen), then by `id` on a tie |
| Bookings | Earliest first, by the appointment's local time in its branch (`localTime`), then by `id` on a tie |

If your branches sit in different time zones, bookings follow each branch's local time, not the UTC instant.

## Does anything change if data is added while I read?

No item you already read repeats and none is skipped because of a new one, since the cursor stores your position as the last item you read, not as a page number.

- **A new customer** added while you read lands at the top of the list, so you will not see it on later pages. Pick it up on your next sync with `since`.
- **A new booking** later than your position shows up on a later page; one earlier than your position shows up on your next read.
- **A booking whose time is moved** while you read jumps to its new place in the order, so you may see it twice or miss it. Use `id` to drop duplicates.

## How do I fetch only what is new instead of the whole list?

Remember where your last sync ended, and ask only for what comes after it.

- **Contacts:** keep the newest `createdAt` you read and send it as `since` on the next sync. `since` includes that very instant, so the last customer you read may come back once; skip it by its id.
- **Bookings:** ask for a window of days with `from` and `to`, such as the next seven days. There is no "changed recently" parameter; to hear about each new or cancelled booking as it happens, use [webhooks](https://docs.mujib.ai/en/developers/webhooks/).
- Keep the selecting parameters the same on every page of one read: the cursor stores your position only, not `since`, `from`, `to` or `status`.

## Frequently asked questions

### Can I jump straight to page three?

No. There are no page numbers. You reach each page with the cursor the page before it returned.

### How do I know I reached the last page?

When nextCursor in the response is null. The last page may hold fewer items than limit.

### Can I change limit between pages?

Yes. The cursor stores your position, not the page size. Keep the parameters that select results, such as since, from, to and status, the same.

### What is the largest page size?

100 items. Without limit, a page holds 50.

---

Source: https://docs.mujib.ai/en/developers/pagination/ · Last updated: 2026-11-04
