> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trassets.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync Cycle: How Data Gets Into the API

> Understand the Databricks-to-RDS sync pipeline behind the Trassets Data API: TRUNCATE+Reload semantics, last_sync_at, and what GET /changes actually detects.

The Trassets Data API is a read-only interface over data that is synced into RDS
PostgreSQL from an upstream pipeline. Understanding how and when that sync happens is
essential for building a correct polling or incremental-sync integration.

## Pipeline architecture

Data flows through three stages: **Databricks Unity Catalog → S3 → RDS PostgreSQL**. The
API has no direct connection to Databricks at runtime — it only ever reads from RDS. The
underlying Databricks jobs are named `parquet_to_s3` and `ingest_parquet_to_rds`.

<Note>
  Trassets' internal documentation describes this sync as running **daily**. This
  repository's public API layer has no machine-enforced schedule of its own to verify
  against — the sync cadence is a property of the upstream pipeline, not something the API
  configures or exposes. Do not assume sub-daily freshness; always check `last_sync_at` (see
  below) rather than assuming a fixed interval has elapsed.
</Note>

## 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 /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

Sync state is tracked per `(customer_id, table_name)` in an internal status table with
these fields: `last_sync_at` (nullable timestamp), `status` (`synced`, `pending`, or
`error`), `row_count`, `primary_key_column`, and `error_message`.

`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 sync (`status = 'error'`) does not
advance it. This means:

* A table with `status = 'pending'` or `status = 'error'` will never appear in
  `GET /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 /changes` actually checks

`GET /changes?since=<timestamp>` returns a table only if **all** of the following hold:

* `status` is exactly `'synced'` (not any other status value)
* `last_sync_at` is not `NULL`
* `last_sync_at > since` — a **strict** greater-than comparison, not `>=`

The `ETag` response header is the maximum `last_sync_at` across all of a customer's synced
tables, so polling with `If-None-Match` lets you skip a request entirely when nothing has
synced since your last poll (see [Delta Sync](/guides/delta-sync) for the full polling
pattern).

<Warning>
  `since` must not be older than 30 days — older values are rejected with a 400 error. This
  limit is enforced in code but not explained anywhere in the API's own documentation or
  source comments; treat it as a hard operational constraint rather than a tunable setting.
</Warning>

## Practical implications for your integration

* **Poll `/changes`, don't guess a schedule.** Because there is no documented, enforced
  sync interval, build your integration to react to what `/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 `/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.
