Skip to main content
The Data API is a read-only interface over data that is synced in from an upstream pipeline. Understanding how and when that sync happens is essential for building a correct polling or incremental-sync integration.
The sync cadence is a property of the upstream pipeline, not something the API configures or exposes — it has no machine-enforced schedule of its own to verify against. Do not assume a fixed refresh interval; always check last_sync_at (see below) instead.

TRUNCATE + Reload semantics

On every sync run, each table is dropped and reloaded in full — there is no row-level incremental update at the table level. This has two direct consequences for integrators:
  1. No partial sync. A table is either fully replaced by the latest sync, or it isn’t touched at all. There is no mechanism to sync “only the rows that changed” within a single table — GET /v1/changes (below) tells you which tables changed, not which rows.
  2. Schema changes are always breaking for cumulative tables. For tables that accumulate history across reloads (for example contract_ends), a schema change cannot be applied additively — TRUNCATE + Reload always replaces the entire table content, so there is no way to migrate old rows to a new shape in place. Any schema change to such a table is treated as a breaking change requiring a new API version, with both the old and new table shapes synced in parallel during a deprecation window.

When last_sync_at is set

last_sync_at is set only when a sync completes successfully for that table — it stays NULL until the first successful sync, and a failed or in-progress sync does not advance it. This means:
  • A table with no successful sync yet will never appear in GET /v1/changes, regardless of how recently a sync was attempted.
  • last_sync_at reflects table-level completion, not the modification time of any individual row inside that table.

What GET /v1/changes actually checks

GET /v1/changes?since=<timestamp> returns a table only if it has completed a successful sync with last_sync_at > since — a strict greater-than comparison, not >=. The ETag response header is derived from the maximum last_sync_at across all of a customer’s synced tables plus the since value (normalised to UTC) and the category filter. A request with a different since or category therefore never matches an ETag issued for another one. Polling with If-None-Match lets you skip a request entirely when nothing has synced since your last poll (see Delta Sync for the full polling pattern).
since must not be older than 120 days — older values are rejected with a 400 error. This limit is documented in the endpoint’s OpenAPI parameter description (see the API Reference tab). It’s a request-hygiene guardrail, not a data-retention limit — treat it as a fixed operational constraint rather than a tunable setting.

Practical implications for your integration

  • Poll /v1/changes, don’t guess a schedule. Because there is no documented, enforced sync interval, build your integration to react to what /v1/changes reports rather than assuming data updates at a fixed time of day.
  • A changed table means “re-fetch everything for this table,” not “here are the diffs.” Row-level change detection does not exist yet — when a table appears in /v1/changes, the safest action is a full re-fetch of that category or raw endpoint.
  • Treat last_sync_at as a freshness signal, not a change log. It tells you the table was successfully reloaded at that time; it does not tell you which rows differ from the previous version.