If you run a managed data source — mirrored from your own database or from a Salesforce object — you own the data and FieldScroll holds a copy. Sooner or later you will want to answer one question: is the copy right?
This page says which fields answer it, which fields look like they answer it and do not, and how to check without reading the whole source. All of it applies to both kinds of managed source. Where it says "your table", read "your Salesforce object" if that is what the source mirrors.
Which field answers which question
| Field | What it tells you |
|---|---|
sync_status | Whether the last sync succeeded, failed, or finished with work left over. |
sync_error | Why the last sync failed. Null when it did not. |
last_synced_at | When a sync last completed. |
updated_at (on the source) | When a sync last ran. Not whether it changed anything. |
data_version | A full-resync signal for sources you upload by hand. Constant on managed sources. |
schema_version | Bumped when the column set changes. |
updated_at (on a row) | When FieldScroll last wrote that row. |
The three below are the ones people misread.
data_version stays at 1 on a managed source, and that is correct
data_version increments when a hand-uploaded CSV replaces a source's rows
wholesale. It means one thing: discard every local row and re-pull. It
exists because a replaced CSV has no per-row history to follow.
A managed source never triggers it. Sync reconciles row by row: changed rows
get a new updated_at, removed rows become tombstones. There is nothing to
discard, so the counter never moves.
A managed source showing data_version: 1 months after creation, with
hundreds of applied row changes, is healthy. It is not a stalled sync, and
mobile clients are not holding a stale copy — they follow row timestamps, not
this counter.
A source's updated_at proves the sync ran, not that it is current
It advances on every sync cycle whether or not a single row changed. It is a liveness signal. Read it as "a sync happened at this time" and nothing more.
This matters more than it sounds. A sync can complete, report ok, advance
updated_at, and still not have applied everything it was asked to — the
delete guardrail declines to remove more than half a source's rows in one
pass. When that happens the source reads Needs attention rather than
Error, so a monitor watching only sync_status === 'error' will not see
it. See managed sources.
A row has two sets of timestamps, and only one is about your data
This is the trap most likely to make an audit report success when it should not.
row.created_atandrow.updated_at— when the sync wrote the row.row.data.updated_at— the value from your table, mirrored as an ordinary column, if your table has such a column.
Comparing your database's timestamps against the outer updated_at only tells
you when FieldScroll last touched the row. That always looks recent. Compare
against row.data.updated_at instead — the mirrored value.
How mobile stays current
Devices delta-sync on row updated_at, pulling everything changed since their
last cursor. Deletions arrive as tombstones rather than absences, so a device
can remove the row locally instead of guessing from a gap. Inserts, edits, and
deletions all reach the device without any version counter moving.
A column-set change bumps schema_version, which rebuilds the local table.
Verifying a source
Three query parameters on
GET /organizations/{organization_id}/data-sources/{data_source_id}/rows make
this practical without walking every page: external_id, updated_after,
and include_deleted.
If you audited a source before August 8, 2026, run it again. Paging through this endpoint used to return only part of a source when its rows carried a display order, which every managed source's rows do. There was no error and the next-page marker looked normal, so a walk that appeared to finish could have been missing rows — in one 25-row test, 12 came back. Any count or coverage conclusion drawn from a full walk before that date is not safe to rely on. Full walks are correct now.
Check one row you know about
external_id is the natural key for a managed source — the same key sync
reconciles on, so you already hold it for every row you push. On a Salesforce
source it is the Salesforce record Id.
curl -sS -H "Authorization: Bearer $FS_KEY" \
"https://fieldscroll.app/api/v1/organizations/$ORG/data-sources/$SRC/rows?external_id=SKU-4471"It is unique per source, so this returns at most one row. An empty data
array means FieldScroll has no row with that key — which is itself an answer.
Check what has changed
updated_after takes an ISO 8601 timestamp and returns only rows written
since. Verification then costs about what your change rate costs, rather than
what your source size costs.
curl -sS -H "Authorization: Bearer $FS_KEY" \
"https://fieldscroll.app/api/v1/organizations/$ORG/data-sources/$SRC/rows?updated_after=2026-08-01T00:00:00Z&include_deleted=true"Pair it with include_deleted=true. A removed row keeps its
updated_at bumped to the moment of deletion, so a deletion is a change — but
without this the row simply stops appearing, and you cannot tell "removed"
from "unchanged". Rows returned this way carry a non-null deleted_at.
By default removed rows are hidden and every row has deleted_at: null. If
you are comparing counts against your own table, that is the behavior you
want; if you are reconciling changes, it is not.
A reconciliation that works
- Read the source and check
sync_status,sync_error, andlast_synced_at. Ignoredata_version. - For each row you expect, request it by
external_idand comparerow.dataagainst your table. Compare timestamps againstrow.data.updated_at, not the outer one. - Ask for
updated_afteryour last check, withinclude_deleted=true, and confirm the changes you made are the changes that landed — and that nothing landed which you did not make. - If counts disagree and no sync reported an error, suspect the delete guardrail before suspecting the data.
Related
- Data sources and lookups — what a managed source is and how sync behaves.
- Pagination — cursors, for a full walk.
- API reference — every parameter on the rows endpoint.