Cursor pagination in the Mujib API with nextCursor
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?
Section titled “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:
{ "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
nextCursorisnull, you have reached the last page. - The last page may hold fewer items than
limit, anddatais 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?
Section titled “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:
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);import osimport timeimport 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?
Section titled “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?
Section titled “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
idto drop duplicates.
How do I fetch only what is new instead of the whole list?
Section titled “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
createdAtyou read and send it assinceon the next sync.sinceincludes 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
fromandto, 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. - Keep the selecting parameters the same on every page of one read: the cursor stores your position only, not
since,from,toorstatus.
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.