202 Accepted with a queued sync request. The re-sync runs
asynchronously — logging into an EHR and re-reading a record is a multi-second
operation on a good day — so you then either poll GET /sync-requests/{id} or
subscribe to the sync_request.completed webhook, and
re-read the resource once the request is terminal.
Supported resource types
resourceId is the id the resource’s own endpoint returns — prefixed
(clm_…, bil_…, pat_…, nte_…, apt_…) from v3, raw UUID earlier. The
prefix must match resourceType.
No new scopes: a re-sync only refills data your app can already read, so it
requires the resource’s existing read scope.
Appointments are coarser. There is no single-appointment sync; an
appointment request re-sweeps the connector’s schedule window (roughly one
week back to six weeks ahead). Because of that it has a longer, per-connector
cooldown — and since the schedule already re-syncs itself every few minutes,
you rarely need it. An appointment outside the window that sweep covers
(roughly one week back to six weeks ahead) is rejected with a 400 rather than
accepted into a re-sync that could not reach it.Request lifecycle
status moves queued → completed | failed. Treat the first terminal status
you see as the outcome. Until the platform has finished recording it, that
status is derived live from the underlying sync runs, so if those runs
briefly become unreadable a later poll can report queued again — even after an
earlier poll returned a terminal status and a completedAt. Polling again
resolves it; the request itself never moves backwards.
completed— every sync run the request fanned out finished. Re-read the resource’s own endpoint for the refreshed data.failed—errorKindsays why, as a closed marker:child_run_failed— a sync run failed, or was interrupted mid-flight. The data may be partially refreshed; retry after the cooldown.cancelled_before_start— the sync runs were discarded while still queued, so nothing this request dispatched ever ran. Usually an equivalent sync already held the lane (in which case that run is doing the same work), but a worker restart looks identical, so treat the resource as possibly still stale. It costs you no daily-cap slot, but it does hold the cooldown (5 minutes, or 30 per connector for an appointment). Re-read the resource, and retry after the cooldown if it is still stale.stale— no terminal outcome inside the watch window (30 minutes). The re-sync may still land late; treat the request as spent and re-read the resource before retrying.
errorKind: it is refused
synchronously with a 4xx or a 502, and no request id is issued. Two kinds,
which differ in what they cost you:
- Refused before the request is recorded — an unknown resource, a paused
connector, an appointment outside the sweep window. Nothing is stored, so
there is no cooldown and no cap slot spent; retrying returns the same
4xxuntil the underlying problem is fixed. - Refused while dispatching, once the request exists — the resource has no
EHR identity, or the dispatch itself failed. These hold the cooldown, so an
immediate retry returns
409.
resourceUpdatedAt on the 202 is the resource’s own change stamp before
your request, so you can tell whether the re-sync changed anything: re-read the
resource and compare. It is the same field that resource’s endpoint returns —
updatedAt for claim, bill, note and patient; lastSyncedAt for
appointment, which exposes that instead and has no updatedAt.
It is read before anything is dispatched, so a re-sync that finishes faster than
the 202 returns cannot slip past it. Two caveats: it is a property of the
resource at request time rather than of the request, so it is returned only on
the 202 (always null on GET); and it moves only when the data actually
changed, so it is a change detector, not a completion signal — use the request’s
status or the webhook for that.
A sync request belongs to the app that created it: GET /sync-requests/{id}
and the completion webhook are both scoped to that app, and another app in the
same organization gets a 404.
Completion webhook
If your app has webhooks configured, a terminal request also emitssync_request.completed, delivered only to the app that made the request:
GET /v{N}/sync-requests/{id}, which accepts either form.
Many resources at once
POST /v4/sync-requests/batch queues up to 25 re-syncs in one round trip.
Kinds may be mixed. Use it when a sweep of your own has found several resources
worth re-checking; it saves round trips, nothing else.
- Results come back in the order you sent them, with
resourceIdechoed back verbatim in the form you sent it, so you can match them to your input. error.statusCodeis the status that item would have received on its own —400,403,404,409,429,502— with the same message. One status is reported per item rather than for the request: a500item is the same unhandled failure the single-resource endpoint would answer with, scoped to that item so the rest of the batch still runs. Retry that item; the others are unaffected.- A
503item means the batch ran out of its time budget before that item was started. An item is not one EHR call — aclaimfans out to as many as four — so a large batch of the heavier kinds can outrun the budget. Nothing ran for a503item: it holds no cooldown and spent no daily-cap slot, so send it again straight away, in a smaller batch. The items that did queue are still in the response with their ids, which is the point of stopping rather than timing out. - Queued items are ordinary sync requests: poll each one’s
GET /sync-requests/{id}, andsync_request.completedfires once per item, never once per batch. - More than 25 items is a
400naming the limit — never a silent truncation. Send the rest in another batch. - The response is held until every item has been dispatched, so a full batch can take several seconds.
- Sending the same resource twice in one batch is allowed; the second occurrence is refused by its own cooldown, like any other repeat.
Guardrails
Re-syncs spend a real clinic’s EHR rate budget, so they are deliberately bounded:- Cooldown — one re-sync per resource per 5 minutes (
409), and a request still running also409s until it finishes or goes stale. Appointments cool down per connector per 30 minutes, since one request sweeps that connector’s whole schedule. - Refunds — a request rejected before it is even recorded (an unknown or
un-syncable resource, a paused connector) costs you nothing at all. Once a
request exists, the daily-cap slot is refunded only when nothing it dispatched
reached the EHR — a
cancelled_before_start, or a refusal that enqueued no work — and the cooldown is held either way, so retrying straight away returns409. - Daily cap — 500 API re-syncs per organization per rolling 24h
(
429), shared across resource types and apps. - The global rate limit applies on top.
409 tells you a refresh is already underway or just happened — poll the
resource instead of retrying the POST.
When not to use this
- Routine change detection. Poll with
updatedSincedelta queries or subscribe to webhooks; background syncs keep the cache fresh without you spending re-syncs. - Bulk refreshes. The batch endpoint exists for a short list you have already narrowed down — it spends one cap slot per item, so it is not a way to refresh everything. If you believe a whole organization’s data is stale, contact us instead of scripting a loop over your cooldown budget.
