Skip to content

5.5.2.3 Sync

The Sync domain of farm. The farm sync client and its runs. The client enrols with a one-time code, reads its feeds and schedules, opens a run by announcing the hashes it holds, uploads only the new files through signed URLs and commits the run; it reports failures and daily digests. Organization users manage their clients and schedules and follow the runs; staff see every run and client. It is one API, with its own contract and its own version.

Feed identity

A SourceFeed is one farm's data arriving through one configured channel: for example, management software, a device, a laboratory, a file, or the Moonitoring app. The feed_id in a path identifies that channel for one farm. It scopes the sync runs, file digests, coverage, source-record keys, and lineage, so Farm can tell which upstream channel produced the data and who is responsible for correcting it. See Sources for the configured channels and Ingestion for how feed-scoped ordering, deduplication, and parsing use that identity.

5.5.2.3.1 Versions

Version Status Prod URL Contract
v1 Proposed https://farm.moonitoring.com/sync/v1 sync/v1/openapi.yaml

The resources follow the data model. Adds the client's enrolment and run handshakes (EnrolmentRedeemRequest, SyncRunOpenRequest, SyncRunCommitRequest, digests and known keys) and SyncRunView, which derives the processing state of a run from its parse jobs as req 52.7 asks. The sync client authenticates as a service account of the farm's Organization; the capability farm.sync_runs.write it needs is req 1002.25.

A person connects the computer by signing in, not by typing a code (decided 2026-11-10). The client signs the person in through the browser (Signing In from a Native App), lists the farms where they hold farm.sync_clients.manage (from GET /organizations/v1/me and the roles in it) and, for the farm they choose, registers a sync client, issues its enrolment code and redeems it at once with the key it generated, all with the person's token. From then on the computer syncs as its own service account, signed in or not. When the person installed the client and chose a farm, the installer has done this already (decided 2026-11-10): it registered the client and issued its code with the person's token and left them in a hand-over bundle, so the client redeems the code at its first start and the person signs in only once. Changing an integration needs the person again, signed in at most five minutes before. Integrations are set up only in the client (decided 2026-11-10), by a person who holds farm.sources.manage: the catalogue of supported software (the software of the active client-side adapter_mappings of GET /sync-clients/me, which lists every one that applies to the farm, global or of its Organization, plus the feed-scoped ones of the client's own feeds, so a newly connected client with no feed has its catalogue too; decided 2026-11-11), the set-up form, Test connection on the computer, then POST /organizations/{organization_id}/feeds of Sources with the person's token and sync_client set to this client; the client reads its feeds with its own token afterwards. The software's credentials stay in the client's Windows Credential Manager and never reach us; a cloud vendor integration that farm pulls is set up the same way, and its credential goes once to PUT /feeds/{feed_id}/credential. GET /sync-clients/me also returns the farm's cloud vendor feeds (cloud_feeds, decided 2026-11-10), read-only, with their last run, so the client shows them as cards although they have no sync_client; removing one is disconnectFeed with the person's fresh sign-in. It offers only the cloud vendors farm can pull (cloud_vendors): the third-party platform channels with an active adapter mapping of side puller that applies to the farm. The writes that check farm.sync_clients.manage ask a person for a sign-in at most five minutes old (otherwise 403 reauthentication-required); reads never ask (decided 2026-11-11), and staff with farm.sync_clients.manage_all are not asked, because staff tokens carry no auth_time. Schedules are set only in the Organization Dashboard (decided 2026-11-10); the client shows them read-only. The client has no name field: it reports the Windows name of its computer (computer_name, req 67.6) with its installation, and the Console and the Dashboard show it to tell computers apart; it is not unique and never an identity, and the client stays known by its UUID. The enrolment code stays for the move from the legacy client and for staff who connect a computer from the Console; the client has no field to type one.

SyncRunResponse is an API view, separate from the persistence schema of SyncRun. An open run or an early failure can return raw_inputs: [] without inventing a manifest, log or counts. A completed delivery requires its finish time, counts and manifest; a failed delivery requires its finish time and failure details. The log is optional because a failure may happen before it can be produced or uploaded. An empty delivery is not proof of an empty source: inferred deletions still require a verified full snapshot, a complete immutable keys manifest, identical resource/filter scope and authoritative source ordering. An explicit empty inventory can satisfy the manifest requirement; a missing manifest cannot.

Enrolment retries use the same Idempotency-Key and request, including the locally generated public key. The recorded receipt is returned even if the consumed code has since expired; another key cannot redeem that code again. After enrolment, farm.sync_clients.read lets the active client read only runs bound to its own SyncClient and a feed still assigned to that client in the same farm. It grants no general access to ingestion data.

5.5.2.3.2 Operations

Every operation names who calls it and the capability it checks (x-moonitoring-audience, x-moonitoring-capability in the contract); every capability is in the catalog of the data model (req 1002, 1006). Lists are paged by cursor, writes that create something carry an Idempotency-Key, errors are Problem Details.

Operation Request Who Capability
Redeem an enrolment code POST /enrolments/redeem Installed clients none
Read my configuration GET /sync-clients/me Installed clients farm.sync_clients.read
Report my installation POST /sync-clients/me/installation Installed clients farm.sync_runs.write
Open a sync run POST /feeds/{feed_id}/sync-runs Installed clients farm.sync_runs.write
Commit a sync run POST /sync-runs/{run_id}/commit Installed clients farm.sync_runs.write
Fail a sync run POST /sync-runs/{run_id}/fail Installed clients farm.sync_runs.write
Renew a running sync run POST /sync-runs/{run_id}/heartbeat Installed clients farm.sync_runs.write
Get an upload link for the run log POST /sync-runs/{run_id}/log-url Installed clients farm.sync_runs.write
Report digests POST /feeds/{feed_id}/digests Installed clients farm.sync_runs.write
List a feed's digests GET /feeds/{feed_id}/digests Organization users, staff or clients farm.ingestion.read or farm.ingestion.read_all
Download the keys we hold GET /feeds/{feed_id}/known-keys Installed clients farm.sync_clients.read
List a farm's sync runs GET /organizations/{organization_id}/sync-runs Organization users, staff or clients farm.ingestion.read or farm.ingestion.read_all
Count a farm's sync GET /organizations/{organization_id}/sync-stats Organization users farm.ingestion.read
Count every farm's sync GET /sync-stats Staff farm.ingestion.read_all
List every sync run GET /sync-runs Staff farm.ingestion.read_all
Read a sync run GET /sync-runs/{run_id} Organization users, staff or clients farm.ingestion.read or farm.ingestion.read_all or farm.sync_clients.read
Get the run log POST /sync-runs/{run_id}/log-download-url Organization users, staff or clients farm.ingestion.read or farm.ingestion.read_all
List a farm's sync clients GET /organizations/{organization_id}/sync-clients Organization users, staff or clients farm.sync_clients.read or farm.sync_clients.read_all
Register a sync client POST /organizations/{organization_id}/sync-clients Organization users, staff or clients farm.sync_clients.manage or farm.sync_clients.manage_all
List every sync client GET /sync-clients Staff farm.sync_clients.read_all
Read a sync client GET /sync-clients/{client_id} Organization users, staff or clients farm.sync_clients.read or farm.sync_clients.read_all
Issue an enrolment code POST /sync-clients/{client_id}/enrolment-codes Organization users, staff or clients farm.sync_clients.manage or farm.sync_clients.manage_all
Retire a sync client POST /sync-clients/{client_id}/retire Organization users, staff or clients farm.sync_clients.manage or farm.sync_clients.manage_all
List a client's schedules GET /sync-clients/{client_id}/schedules Organization users, staff or clients farm.sync_clients.read or farm.sync_clients.read_all
Add a schedule POST /sync-clients/{client_id}/schedules Organization users, staff or clients farm.sync_clients.manage or farm.sync_clients.manage_all
Change a schedule PATCH /schedules/{schedule_id} Organization users, staff or clients farm.sync_clients.manage or farm.sync_clients.manage_all
Delete a schedule DELETE /schedules/{schedule_id} Organization users, staff or clients farm.sync_clients.manage or farm.sync_clients.manage_all

5.5.2.3.3 Live Updates

A WebSocket at /sync/v1/live follows the rules for live updates. The Organization Dashboard shows a sync as it happens: a run opened, files uploaded, the commit, then each parse job finishing. Polling the runs list would arrive late and cost a request per second per open screen. The socket carries updates only: sync_run.committed.v1, sync_run.failed.v1 and parse_job.finished.v1 for the Organization's feeds, as they are published; the screen loads its state through listOrganizationSyncRuns and reloads it when it reconnects. The connection is authenticated with the same token as REST, closed when the screen is not visible, and reconnects with growing delays.

5.5.2.3.4 Contract v1

Proposed version

v1 is a proposal for review: its endpoints, errors and capabilities were designed from the requirements and the flows and are not confirmed. It leaves proposal with its first release; from then on it changes only as described in Every API Is Versioned.

5.5.2.3.5 Rules Confirmed with the Implementation

Decided on 2026-11-05, from the choices made while building farm:

  • Defaults: a run has six hours (deadline_at) and a heartbeat timeout of ten minutes; an enrolment code lasts 24 hours and is 15 characters of Crockford base 32 in three groups; a feed is silent after its schedule's longest gap plus two hours of grace.
  • A running run names its client adapter, or the adapter call answers 422. A source no adapter covers ends the run with failure_class: unsupported_source, which records an occurrence of the integration gap from the detection; a failed run carries no file.
  • An abandoned run has no failed_step: the sweeper cannot know where the client stopped.
  • A file uploaded by hand is a delivered run with kind: full_snapshot, trigger: manual, no client and zero counts, because raw inputs and their processing state live on runs.
  • A code issued by staff gives a service account owned by the farm's owner, with the global Sync Client role found by its stable key in the copy of access, never by its display name.
  • The device of an announced file stays on the run's announcement.
  • Detection is kept per run and per feed: the product, version and fingerprint each feed was seen with, first and last, with the last run and a count (FeedDetection), so staff choose strategies per detected version.
  • Parse jobs get five attempts for transient failures, with a delay doubling from 30 seconds up to an hour, and a lease of ten minutes. A retry is a refused message, so Pub/Sub holds the later files of the same feed meanwhile and only that feed waits. A job out of attempts is failed and published as parse_job.dead.v1 through the outbox; the parse subscription has no dead-letter policy, so the order per feed holds.
  • A parser is found by the code and version of its parser adapter. A job whose adapter no worker carries fails as unsupported_version and records a parser gap with the file as its sample. Until the canonical domains exist, a parser that returns records fails the job as dependency_unavailable rather than dropping them.