Skip to content

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.

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.)

ParameterMeaning
limitItems per page, from one to 100. Without it, 50
cursorThe 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.

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 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))

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

ListOrder
ContactsNewest first, by createdAt (when the customer was first seen), then by id on a tie
BookingsEarliest 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 id to 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 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.
  • 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.