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

# On-demand sync

> Ask for one re-sync of a specific resource with the source EHR

Every read endpoint serves a **cache** of the clinic's EHR, refreshed
continuously by background syncs. Most of the time that cache is minutes fresh
and you never think about it. When you *know* something changed in the EHR —
a biller tells you a claim was corrected, your user is looking at a stale
ledger — you can ask for one targeted re-sync instead of waiting for the next
background pass:

```http theme={null}
POST /v4/sync-requests
{
  "resourceType": "claim",
  "resourceId": "clm_a1b2c3d456784abc9def0123456789ab"
}
```

The response is `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](/guides/webhooks), and
re-read the resource once the request is terminal.

## Supported resource types

| `resourceType` | What gets re-synced                                                                                                                                                               | Required scope      |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `claim`        | The claim and its bill. The patient's insurances and any unsigned note are refreshed alongside on a best-effort basis — they are not part of what the request's status reports on | `read:claims`       |
| `bill`         | The bill and its line items                                                                                                                                                       | `read:bills`        |
| `patient`      | Demographics and insurance policies                                                                                                                                               | `read:patients`     |
| `note`         | The visit note (content and signature status), including one already signed                                                                                                       | `read:notes`        |
| `appointment`  | The connector's **whole schedule window** (see below)                                                                                                                             | `read:appointments` |

`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.

<Note>
  **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.
</Note>

## Request lifecycle

```json theme={null}
{
  "code": "success",
  "data": {
    "id": "srq_c1d2e3f456784abc9def0123456789ab",
    "resourceType": "claim",
    "resourceId": "clm_a1b2c3d456784abc9def0123456789ab",
    "status": "queued",
    "errorKind": null,
    "resourceUpdatedAt": "2026-08-15T00:00:00.000Z",
    "requestedAt": "2026-09-01T12:00:00.000Z",
    "completedAt": null
  }
}
```

`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`** — `errorKind` says 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.

A request that cannot be started at all never reaches `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 `4xx`
  until 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 emits
`sync_request.completed`, delivered only to the app that made the request:

```json theme={null}
{
  "type": "sync_request.completed",
  "data": {
    "id": "c1d2e3f4-5678-4abc-9def-0123456789ab",
    "resourceType": "claim",
    "resourceId": "a1b2c3d4-5678-4abc-9def-0123456789ab",
    "organizationId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "status": "completed",
    "errorKind": null,
    "requestedAt": "2026-09-01T12:00:00.000Z",
    "completedAt": "2026-09-01T12:01:30.000Z"
  },
  "timestamp": "2026-09-01T12:01:30.000Z"
}
```

The webhook is best-effort ([like every data event](/guides/webhooks)); polling
is the correctness path. Ids in webhook payloads are raw UUIDs — pass them
straight to `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.

```http theme={null}
POST /v4/sync-requests/batch
{
  "requests": [
    { "resourceType": "bill",  "resourceId": "bil_b9c6ac7d…" },
    { "resourceType": "bill",  "resourceId": "bil_befbbc4c…" },
    { "resourceType": "claim", "resourceId": "clm_a1b2c3d4…" }
  ]
}
```

Every item is processed **independently, under exactly the same guardrails** as
the single-resource endpoint: its own scope check, its own cooldown, and its own
slot of the 500-per-organization-per-24h ceiling. A batch of 25 spends 25 slots,
not one — batching buys round trips, not budget.

<Warning>
  **Partial success is the normal case.** The `202` says the batch was processed,
  not that every item queued. Each result carries either `request` or `error`,
  never both and never neither — check every one. An item refused by its cooldown
  does not affect the others.
</Warning>

```json theme={null}
{
  "code": "success",
  "data": {
    "requests": [
      {
        "resourceType": "bill",
        "resourceId": "bil_b9c6ac7d…",
        "request": {
          "id": "srq_c1d2e3f456784abc9def0123456789ab",
          "status": "queued",
          "resourceUpdatedAt": "2026-07-06T00:00:00.000Z",
          "…": "…"
        },
        "error": null
      },
      {
        "resourceType": "bill",
        "resourceId": "bil_befbbc4c…",
        "request": null,
        "error": {
          "statusCode": 409,
          "message": "This bill was already re-synced in the last 5 minutes. Retry after the cooldown."
        }
      }
    ]
  }
}
```

* Results come back **in the order you sent them**, with `resourceId` echoed
  back verbatim in the form you sent it, so you can match them to your input.
* `error.statusCode` is 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: a `500` item 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 `503` item means the batch ran out of its time budget before that item was
  started. An item is not one EHR call — a `claim` fans out to as many as four —
  so a large batch of the heavier kinds can outrun the budget. Nothing ran for a
  `503` item: 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}`, and `sync_request.completed` fires **once per
  item**, never once per batch.
* More than 25 items is a `400` naming 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 also `409`s 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 returns
  `409`.
* **Daily cap** — **500** API re-syncs per organization per rolling 24h
  (`429`), shared across resource types and apps.
* The global [rate limit](/guides/rate-limits) applies on top.

A `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
  [`updatedSince` delta queries](/guides/pagination) or subscribe to
  [webhooks](/guides/webhooks); background syncs keep the cache fresh without
  you spending re-syncs.
* **Bulk refreshes.** The [batch endpoint](#many-resources-at-once) 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.
