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..."
}
dataholds the items of the page.hasMoreistrueas long as items remain after this page.nextCursoris the cursor of the next page, ornullon the last page.
Parameters
| Parameter | Purpose |
|---|---|
limit | Number of items per page, from 1 to 100. 25 by default. |
cursor | Value 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
| Filter | Lists | Effect |
|---|---|---|
establishmentId | reviews, private feedback, contacts | Keeps only the items of this location. |
createdAfter | reviews, private feedback, contacts | Keeps only the items created from this date, inclusive. |
createdBefore | reviews, private feedback | Keeps only the items created up to this date, inclusive. |
includeDeleted | reviews | true 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:
- Keep the most recent
createdAtyou received, per resource. - On the next run, send it as
createdAfter. Since the bound is inclusive, the last item you already have comes back: deduplicate byid, updating the existing item rather than adding it again. - 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 updatesupdatedAt, the analysis fillsanalysis.sentimentonce the review has been analyzed, and a deletion on the platform fillsdeletedAt. Read a recent sliding window again on each run, for instance the last 30 days, and list deleted reviews withincludeDeleted=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 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:
- Run a first full sync with the lists, as above.
- Then read
GET /v1/events(events:readscope), keeping thenextCursorposition 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
nextCursoryou received asafteron the next call. It is nevernull: whenhasMoreisfalse, you are up to date, and that samenextCursoris 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
typesandestablishmentIdfilters. - Each event carries the related object in its public shape (
data.object): a review, a private feedback or a contact. Update your copy by itsid. - 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

