Skip to content

Incremental Sync ​

Five 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 ​

Three formats are accepted: ISO 8601 with offset (2026-08-01T00:00:00+02:00), ISO 8601 without offset (2026-08-01T00:00:00), or 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.

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 keeps that offset unchanged, including in the returned timestamps.

Sorting in filter mode ​

Without a timestamp filter, each endpoint keeps its own default sorting, as described in that endpoint's article. As soon as you set a timestamp filter, sorting switches to internal ID ascending. This order 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. Paginate until a page contains fewer than limit records or is empty instead.

When a full sync is still required ​

An incremental sync does not always replace a full sync. Three situations remain:

  • Endpoints without a deletion timestamp, such as invoices and fee notes, never report deletions. Only a periodic full sync shows which records disappeared.
  • A restored record disappears from the deletion sync. It reappears in the change sync instead, provided your client evaluates the deleted field.
  • If meta.total is 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.

Offisy GmbH