Incremental Sync and Pagination - MerchantFlow Public API
How to backfill and stay current with the MerchantFlow Public API using cursor pagination and updated_after, including why order economics change after an order is placed.
Incremental sync and pagination
This is the page to read carefully. Getting sync right is most of the work in a MerchantFlow integration, and the part that most often goes subtly wrong.
The thing that makes this different
An order's economics change after it is placed. This is not an edge case; it is the normal life of an order in MerchantFlow:
- The merchant corrects a product's cost, so the order's COGS and profit change.
- A 3PL reports the actual fulfilment cost three days after shipping.
- Ad spend syncs and gets allocated across the day's orders.
- Attribution is enriched, so the order's UTM data changes.
- A refund lands.
If you sync on "orders created since yesterday", you will capture each order once with whatever numbers happened to exist that morning, and never see any of the above. Your profit figures will drift away from the merchant's dashboard and you will not be able to tell why.
updated_after exists precisely for this. It tracks a change marker that advances
whenever anything you can read about an order changes - including every case
above.
Backfilling
Page with cursors until page.has_more is false.
curl "https://merchantflow.ai/api/v1/orders?limit=250" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"data": [ /* up to 250 orders */ ],
"page": {
"has_more": true,
"next_cursor": "eyJ2IjoxLCJrIjoiZGF0YVZlcnNpb25BdCIsInQiOiIyMDI2...",
"limit": 250
}
}Then:
curl "https://merchantflow.ai/api/v1/orders?limit=250&cursor=eyJ2IjoxLCJrIjoi..." \
-H "Authorization: Bearer $ACCESS_TOKEN"Repeat every filter on every page. A cursor is bound to the exact filter set
it was created with. Sending it with different filters returns
400 CURSOR_FILTER_MISMATCH rather than silently giving you a page computed
against the wrong query. That error is a guardrail, not a bug - it is telling you
your pagination loop dropped a parameter.
Cursors are opaque. Do not parse, construct, or store them long-term; they are for the duration of one pagination run.
Staying current
Record the updated_at of the last order you successfully processed. On the next
run:
curl "https://merchantflow.ai/api/v1/orders?updated_after=2026-09-02T23:55:00Z&limit=250" \
-H "Authorization: Bearer $ACCESS_TOKEN"Results come back oldest change first. That ordering is deliberate: if your sync dies halfway, the watermark from the last successfully processed record is always safe to resume from - you can never skip a record by crashing.
Two rules that matter
Resume from your watermark minus a few minutes. Five is a reasonable default. MerchantFlow writes concurrently from several workers, so change markers are not perfectly monotonic across processes. A small overlap costs you a handful of duplicate records and protects you from missing one.
Treat the feed as at-least-once. You will occasionally receive an order you
have already seen, unchanged. Upsert on id (or external_order_id) rather than
inserting, and this is a non-event.
A minimal loop
watermark = load_watermark() # ISO 8601, or None on first run
params = {"limit": 250}
if watermark:
params["updated_after"] = shift_back(watermark, minutes=5)
cursor = None
while True:
if cursor:
params["cursor"] = cursor
body = get("/api/v1/orders", params).json()
for order in body["data"]:
upsert(order) # idempotent on order["id"]
watermark = order["updated_at"] # oldest-first, so this only moves forward
if not body["page"]["has_more"]:
break
cursor = body["page"]["next_cursor"]
save_watermark(watermark)Save the watermark only after the page is fully processed. If you save it per record and then crash mid-page, you are still correct - that is the point of oldest-first ordering.
Products and profitability
GET /api/v1/products supports updated_after the same way.
GET /api/v1/profitability/daily is paginated by date rather than by change
marker. Re-fetch a trailing window - the last 30 to 60 days is usually right -
rather than only fetching new days, because a day's profit changes when costs
behind it change.
Deletions
updated_after cannot express a deletion, and MerchantFlow does not have a
deletion feed.
In practice orders are cancelled, not deleted. A cancellation does advance
the order's change marker, but under the default status filter the cancelled
order is excluded from the results - so a client polling with the default
filter never receives the record with canceled_at populated. The order simply
stops appearing.
If cancellations need to propagate to your side, poll updated_after with
status=all (and repeat that filter on every cursor page). The cancelled order
then comes back with canceled_at set, and you can mark it accordingly. If you
stay on the default filter, treat the periodic reconciliation below as the
only way you learn about cancellations.
Rows genuinely can disappear - a merchant closing their account, or a full data
resync rebuilding history. Those will not appear in your incremental feed. If
exactness matters to you, run a full reconciliation periodically (monthly is
usually enough): page the whole order list without updated_after and mark
anything absent as gone on your side.
Plan history windows
A merchant's plan bounds how far back they - and therefore you - can read.
| Plan | History |
|---|---|
| Starter | 90 days |
| Pro | 365 days |
| Plus | Unlimited |
This is not a restriction on partner applications specifically; it is the same window the merchant sees in their own dashboard. Showing them data they cannot see themselves would be incoherent.
Two response fields tell you where you stand:
meta.history_available_from- the earliest date included, ornullfor unlimitedmeta.truncated-truewhen the plan window cut off part of the range you asked for. On Starter and Pro this is alsotruewhenever you omitcreated_after, because an open-ended request always reaches past the window
truncated: true is not an error. Everything from history_available_from
onwards is present and complete. If you are showing a merchant a chart that
starts abruptly, this is why, and saying so is better than leaving them to guess.
Rate limits
Per store, per application:
| Limit | Value |
|---|---|
| Burst | 60 requests/minute |
| Hourly | 1,000 requests |
| Daily | 10,000 requests |
| Concurrent | 2 requests in flight |
And per application across all connected stores: 200 requests/minute.
A 100,000-order backfill at 250 per page is roughly 400 requests, so it fits comfortably inside an hour. A nightly incremental sync for one store is usually single digits.
Two of these catch specific mistakes:
Concurrency (2). Page cursors sequentially. Firing many parallel requests at one store will hit this immediately, and parallelism does not help anyway - a cursor page depends on the one before it.
Per-application global (200/min). If you connect 500 merchants and start every sync at 02:00, you will hit this even though each individual store is well within budget. Stagger your schedule - jitter the start time per connection.
On 429 you get Retry-After (seconds) and X-RateLimit-Reason, one of burst,
hour, day, concurrency, client_global. Back off on the named limit rather
than retrying blindly. Successful responses carry RateLimit-Limit,
RateLimit-Remaining and RateLimit-Policy for the hourly budget, so you can
pace yourself before hitting a wall.
Caching
/api/v1/account, /api/v1/stores and /api/v1/profitability/summary return
Cache-Control. Honour it - these are the endpoints most likely to be polled and
least likely to change, and respecting the header is free headroom against your
budget.
Next
Last updated: September 22, 2026
Last updated on
Public API Scopes - What Each Permission Grants
The four read-only scopes a MerchantFlow partner application can request, what data each unlocks, and how merchants see them on the consent screen.
Public API Endpoint Reference - Orders, Products, Profitability
Complete reference for the MerchantFlow Public API: account, stores, orders with order-level economics, products with COGS, and profit and loss.