Pagination and incremental sync

Walk through a list page by page with a cursor, then fetch only what is new on each run.

API documentationGuides · Pagination and sync

Shape of a list

Every list operation returns the same object:

{
  "object": "list",
  "data": [{ "object": "review", "id": "jd7f3k2m9x8w1q4z" }],
  "hasMore": true,
  "nextCursor": "eyJ2IjoxLCJjIjoi..."
}
  • data holds the items of the page.
  • hasMore is true as long as items remain after this page.
  • nextCursor is the cursor of the next page, or null on the last page.

Parameters

ParameterPurpose
limitNumber of items per page, from 1 to 100. 25 by default.
cursorValue of nextCursor received with the previous page. Omitted for the first page.

The cursor is opaque: do not decode it and do not build one. It can only be reused with the same filters as the request that returned it: changing a filter midway answers 400 invalid_cursor. The limit value, however, can change from one page to the next.

An unknown, repeated or out-of-range parameter answers 400 invalid_request, with the details in errors (for instance query.limit: “Must be less than or equal to 100”).

Order of items

Reviews, private feedback and contacts are listed from the most recent to the oldest. For the other lists, do not rely on any particular order.

Walking through every page

In JavaScript, in a Node 18+ module:

async function listAll(path, params = {}) {
  const items = [];
  let cursor = null;
  do {
    const query = new URLSearchParams({ ...params, limit: "100" });
    if (cursor) query.set("cursor", cursor);
    const response = await fetch(`https://api.mabellenote.com/v1${path}?${query}`, {
      headers: { Authorization: `Bearer ${process.env.MBN_API_KEY}` },
    });
    const page = await response.json();
    if (!response.ok) throw new Error(`${page.status} ${page.code} (${page.requestId})`);
    items.push(...page.data);
    cursor = page.nextCursor;
  } while (cursor);
  return items;
}

const reviews = await listAll("/reviews", { rating: "1" });

In Python, with requests:

import os
import requests

def list_all(path, **params):
    items, cursor = [], None
    while True:
        query = {**params, "limit": 100}
        if cursor:
            query["cursor"] = cursor
        response = requests.get(
            "https://api.mabellenote.com/v1" + path,
            params=query,
            headers={"Authorization": "Bearer " + os.environ["MBN_API_KEY"]},
            timeout=30,
        )
        page = response.json()
        if not response.ok:
            raise RuntimeError(f"{page['status']} {page['code']} ({page['requestId']})")
        items.extend(page["data"])
        cursor = page["nextCursor"]
        if cursor is None:
            return items

Each page counts as one call against the rate limit: reading 1,000 reviews in pages of 100 costs 10 calls.

Filters useful for syncing

FilterListsEffect
establishmentIdreviews, private feedback, contactsKeeps only the items of this location.
createdAfterreviews, private feedback, contactsKeeps only the items created from this date, inclusive.
createdBeforereviews, private feedbackKeeps only the items created up to this date, inclusive.
includeDeletedreviewstrue lists only the reviews deleted from the platform.

Dates use the ISO 8601 format, for instance 2026-10-24 or 2026-10-24T09:15:02.311Z. The reference lists every filter of every operation.

Incremental sync

A first sync walks through every page. The following ones only ask for what was created since:

  1. Keep the most recent createdAt you received, per resource.
  2. On the next run, send it as createdAfter. Since the bound is inclusive, the last item you already have comes back: deduplicate by id, updating the existing item rather than adding it again.
  3. Walk through the pages until hasMore: false.

An item you already synced can still change:

  • Reviews: a published reply fills reply, an edit by its author updates updatedAt, the analysis fills analysis.sentiment once the review has been analyzed, and a deletion on the platform fills deletedAt. Read a recent sliding window again on each run, for instance the last 30 days, and list deleted reviews with includeDeleted=true.
  • Contacts: consents, opt-outs and visits update updatedAt. No filter applies to that date: sync the whole customer base again at regular intervals.
A publication date, not an arrival date

A review's createdAt is its publication date on the platform, not the date it reached Ma Belle Note. When a location joins the account, or when its history is imported, older reviews appear with past dates. Watch GET /v1/establishments: a new id, or a reviews.count that grows more than what you received, calls for a full resync of that location with establishmentId.

Follow changes with the event log

Rereading a sliding window has its limits: it costs calls and does not see every change. The event log follows every change of the account, in the order it is published. It is the most reliable way to stay in sync:

  1. Run a first full sync with the lists, as above.
  2. Then read GET /v1/events (events:read scope), keeping the nextCursor position from one call to the next.
curl "https://api.mabellenote.com/v1/events?types=review.created,review.updated,review.reply_published&limit=100" \
  -H "Authorization: Bearer $MBN_API_KEY"
  • Events come oldest first. Without after, reading starts from the oldest retained event.
  • Pass the nextCursor you received as after on the next call. It is never null: when hasMore is false, you are up to date, and that same nextCursor is used for the next run. Keep it from one sync to the next.
  • As with lists, a position can only be reused with the same types and establishmentId filters.
  • Each event carries the related object in its public shape (data.object): a review, a private feedback or a contact. Update your copy by its id.
  • Events stay readable for 30 days. An integration stopped for longer starts again from a full sync.
  • Events are only produced while at least one API key or webhook endpoint of the account is active, and never for earlier facts.

GET /v1/events/{eventId} reads one specific event. To be notified without polling the API, webhooks push these same events to your server, and the log then serves for catching up.

Updated on October 25, 2026