Skip to content

Incremental sync with updated_after

You want a copy of the clients, the sales or the service orders in your system, and you want to keep it current without downloading everything again every hour. That is what the updated_after filter is for: it returns only what changed since a given instant, in the order it changed.

The examples use clients, but the same applies to every list whose object has updated_at.

Every list accepts updated_after. With it, three things change:

  • The cut. Only records with updated_at after the instant you sent come back. The bound is exclusive: a record whose updated_at equals the value you sent does not come.
  • The order. The list is sorted by updated_at ascending, from what changed longest ago to what changed just now. Without updated_after, the order is created_at descending.
  • The tie-break. Two records with the same updated_at always come in the same order, by id. That is what keeps pagination from skipping or repeating records between pages.

created_after and created_before are exclusive too, and can be combined with updated_after.

The cursor remembers the sort mode. So send the same filters on every page of the same run, only adding cursor. A cursor from the updated_at order used in a call without updated_after returns 422: switching modes midway means starting over. See Pagination.

  1. On the first run there is no starting point. Send a very old instant, such as 1970-01-01T00:00:00Z: that returns everything, already in sync order.
  2. Read page by page, following next_cursor, until has_more is false.
  3. Write each record on your side by id, as an upsert: if it exists, overwrite; if not, create.
  4. After each page, store the largest updated_at you saw. That is the starting point of the next run.
  5. On the next run, start a little before that point, with an overlap window.

Sending exactly the largest updated_at you saw works almost always. The gaps are at the edges:

  • a record written at the same instant as the last one you read, but not yet committed when your read went by, is left out, because the bound is exclusive;
  • a long write can get an updated_at a little earlier than the moment it becomes visible to reads.

Going back a few minutes covers both cases. We recommend 5 minutes: the cost is rereading a few records each run, and the write by id in step 3 already makes rereading harmless. If you want to skip the repeated work, compare the incoming updated_at with the one you have and ignore it when they are equal.

The window also protects you when your process dies midway through a run: you restart from the last stored point, minus the window, and nothing is left behind.

Terminal window
# One page of the run. The starting point is the largest updated_at of the
# previous run, minus the overlap window.
curl -G "https://api.fatureihoje.com/public/v1/clients" \
-H "Authorization: Bearer $FH_API_KEY" \
--data-urlencode "updated_after=2026-09-24T12:55:00Z" \
--data-urlencode "limit=100"
# The next page: the SAME filters, plus the next_cursor that came back.
curl -G "https://api.fatureihoje.com/public/v1/clients" \
-H "Authorization: Bearer $FH_API_KEY" \
--data-urlencode "updated_after=2026-09-24T12:55:00Z" \
--data-urlencode "limit=100" \
--data-urlencode "cursor=NEXT_CURSOR_VALUE"

The loop respects 429: it waits for Retry-After and retries the same page. A first run over a large base uses a good part of the company’s request budget, which is a single one for all of its keys; use limit=100 and run it outside peak hours. See Rate limits.

Deletions. A deleted record disappears from the list, so it never shows up in a run. The same goes for a deleted service order, which is archived and starts returning 404. To learn what went away:

  • subscribe to the area’s .deleted events (client.deleted, service_order.deleted and the others in the Event catalog);
  • or, from time to time, compare the full list of ids with your copy.

A deletion can also touch other records: when a client is deleted, their tasks and appointments lose the link. When you get a .deleted, reread whatever in your system pointed to the deleted record.

Lists without updated_at. Three lists have objects that do not change:

  • GET /public/v1/stock_movements and GET /public/v1/events: stock movements and events do not change once written. There, updated_after cuts by the creation instant, in ascending order, and the recipe above works the same using created_at as the starting point.
  • GET /public/v1/team: the object carries no date. The list is small; read all of it when you need it.

Sync and webhooks do not compete. Each one covers the other’s weak spot:

WebhookSync
When it arrivesRight awayOn the next run
If your server is downRetries for about 3 days, then it is lostRestarts from the last stored point
Endpoint turned offEvents from that period do not arriveDoes not depend on an endpoint
Changes without an eventNo noticeBrings them, if updated_at changed

The design that works: the webhook notifies, the sync guarantees. Use the event to act right away (read the object and update your side) and run the sync from time to time, once an hour or once a day, as a safety net. Both write by id, so getting the same change through both paths does no harm.

See Webhook best practices and Deliveries and retries.