The job

Your analytics team wants every fileAI document, with its status and classification, in a raw_files table that refreshes every 15 minutes. Each run should fetch only what changed since the last one, and never lose or double-count a file.

Sample input

A workspace with about 40,000 files, of which a few hundred change between runs: new uploads, re-processed files, edited values. A small sync_state table in your warehouse holds one row: the watermark from the last successful run, 2026-10-15T23:45:00.000Z.

Steps

The cURL commands use two environment variables:
See Regions for regional hosts.
1

Read the watermark

Load the last saved updatedAt from sync_state. On the first run there is none: omit updatedAfter and load everything.
2

Walk every changed file with a cursor

Call GET /v1/files with updatedAfter, sorted by updatedAt ascending, in cursor mode. Send cursor empty on the first page, then follow pagination.nextCursor until hasMore is false. Keep every other parameter identical for the whole walk: a cursor is bound to its filters and sort, and changing them returns 400.
If the walk fails part-way, start again from the same watermark with an empty cursor. Nothing is lost, because the watermark has not moved.
3

Deduplicate and merge on fileId

updatedAfter is inclusive (>=), so the file at the watermark comes back again, and a file edited during the walk can appear twice. Stage the rows, keep the newest updatedAt per fileId, and merge. In Snowflake:
Snowflake
4

Advance the watermark

Only after the merge commits, save the largest updatedAt you loaded. Keep the old value if the run returned nothing.
Python
updatedAt is an ISO 8601 UTC string with millisecond precision, so the largest string is also the latest time.

Complete job

The steps above as one script you can schedule. It assumes three tables in your warehouse and a DB-API connection conn with autocommit off (for example from snowflake.connector.connect(...)).
Snowflake tables
sync_files.py
Run sync(conn) every 15 minutes from your scheduler. If it raises, roll back: nothing is committed, and the next run starts again from the same watermark.

Remove deleted files

Deleted files never appear in GET /v1/files, so an incremental run can’t see them. Once a day, or as often as you need, walk every file with no updatedAfter and delete warehouse rows that didn’t come back:
Python
A full walk of a large workspace makes many requests; pace it with the rate-limit headers.

Result

Illustrative: shape from the API reference. Rows are shortened to the fields this job loads.
GET /v1/files?updatedAfter=…&sortBy=updatedAt&sortOrder=ASC&limit=100&cursor=
The first row has updatedAt equal to the watermark: it was loaded last run and comes back because the filter is inclusive. The merge leaves it unchanged. The new watermark is 2026-10-15T23:58:21.744Z.

What it shows

  • A stored watermark plus updatedAfter keeps each run small, whatever the size of the workspace.
  • Cursor mode keeps the walk stable while files change. See Pagination for why legacy page/limit is a poor fit for syncs.
  • Inclusive filtering plus deduplication on fileId makes overlap between runs harmless.
  • Advancing the watermark last makes a failed run safe to repeat.
Deleted files never appear in GET /v1/files; use Remove deleted files to clear them from your warehouse. To load extracted values as well, call GET /v1/files/{fileId}/values for each changed fileId after the merge and read formValues from the response. Number cells whose value is 0 come back as null. See the financial report example and Data types.

Next steps

Incremental loading

Filters, what changes updatedAt, and every file field.

Pagination

Cursor rules, limits and the legacy mode.

Rate limits

Pace a large backfill and handle 429.

Troubleshooting

Cursor 400s and other common failures.