Incremental Sync
Seven endpoints support timestamp filters for an incremental sync: createdSince, createdUntil, updatedSince, updatedUntil, and deletedSince. Which filters are available on which endpoint is listed in that endpoint's parameter table.
Accepted time formats
ISO 8601 with minute or second precision is accepted, each with or without a time zone offset, or as UTC with Z (2026-08-01T00:00+02:00, 2026-08-01T00:00:00, 2026-08-01T00:00:00Z), plus a plain date (2026-08-01). Fractional seconds are also accepted (2026-08-24T10:00:00.123+02:00).
This is exactly the format new Date().toISOString() produces in JavaScript. Common Rust and Python serializers produce the same format.
For a plain date, the API sets *Since parameters to 00:00:00 and *Until parameters to 23:59:59. If an endpoint does not support a parameter, the API silently ignores it.
Invalid values
An invalid value is rejected with 400 validation_error, not corrected. So the API does not silently shift 2026-02-30 to March 2.
If createdSince is after createdUntil, or updatedSince is after updatedUntil, the API also responds with 400 validation_error. Equal values are allowed.
Time zones
A value without a UTC offset is treated as the server's local time. If your own time zone differs, the query window shifts by that difference. At the sync boundary, you then miss changes or receive them twice.
Always send the value with an offset, for example 2026-08-01T00:00:00+02:00. The API converts the value to the server time zone internally. The returned timestamps are also in the server time zone.
Sorting in filter mode
Without a timestamp filter, lists sort by internal ID ascending (see Conventions). As soon as you set a timestamp filter, this sorting stays the same. It stays stable across the entire pagination run.
A record can therefore reach you twice at most, but never gets skipped. Upsert your data by id — that makes duplicates harmless.
Cursor for the next run
In filter mode, the API sorts by internal ID, not by timestamp. The last element of a page therefore does not carry the most recent timestamp. If you build your cursor from it, you skip changes.
Use the largest timestamp across all pages of the entire run instead. Only set it once the run has completed in full.
If the run aborts, keep the old cursor and repeat the run completely. Also subtract a small safety margin from the new cursor, around one minute.
Residual risk of pagination
A record can still be skipped if it leaves the result set during pagination. The set then shrinks, and everything behind it moves up one position.
The record at the page boundary drops out of the run this way. Its timestamp stays unchanged and sits below the cursor you set afterwards — so it never gets picked up again.
This typically happens when a record is edited beyond a set updatedUntil boundary during the run. So leave updatedUntil out for incremental syncs.
meta.total with restricted permissions
total is counted in the database, and the permission check only applies afterwards. If your API access has restricted permissions, the pages contain fewer records than total suggests.
A stop condition like "paginate until all total records have arrived" therefore never terminates. The permission check only removes rows from the page that limit already cut. So a short or empty page does not automatically mean the end of the data.
Paginate by offset instead:
- Increase
offsetbylimiton every request. - Repeat as long as
offset < total. - A page can return fewer records than
limit— even come back empty — without the data being exhausted. - Stop once
offset >= total.
When a full sync is still required
An incremental sync does not always replace a full sync. Three situations remain:
- Invoices have no deletion timestamp of their own, but deleting one sets
updatedAt. A deletion is therefore detectable incrementally throughupdatedSince— the record comes back withdeleted: true. Fee notes only supportcreatedSince/createdUntil, and deleting their invoice does not change the fee note itself. Deletions stay invisible incrementally here, so a periodic full sync usingstatus=deletedremains necessary. - A restored record disappears from the deletion sync. It reappears in the change sync instead, provided your client evaluates the
deletedfield. - If
meta.totalis larger than the amount your permissions actually deliver, some records stay hidden. You only catch this with a full sync without a timestamp filter.
Details on deletion timestamps and restoration are covered in each endpoint's own article.