Ingestion
What happens between a 202 and a settled submission: staging, the work that follows, how re-sending the same content behaves, and what each status actually tells you.
Ingestion
Every clinical record you send arrives as a submission. One call is one submission and one
receipt, and that receipt's ingestionId is the only handle you need to keep.
Submitting is covered by the two how-to guides — FHIR and CDA. This page is about what happens afterwards, and why a submission behaves the way it does.
Accepted is not finished
A successful ingest call answers 202, not 200. What completed by the time you get that
response is narrow and worth being precise about:
- The payload was parsed and validated. If it was malformed, you got a
4xxinstead and nothing was stored. - Everything usable in it was staged — written down durably, so it cannot be lost.
- An account of what arrived was recorded as
intake. - Work was scheduled to turn what was staged into the resolved record.
What has not happened is that work. Nothing you sent is queryable when the call returns. Entities have not been deduplicated against the rest of the patient's record, relationships have not been inferred, and the search index does not know about any of it.
This is why the receipt carries a status and why you poll it.
There are no webhooks
Polling the receipt is the only way to follow a submission. There is no callback, and nothing will notify you when a submission settles.
Three timestamps, three different questions
The receipt carries receivedAt, startedAt and completedAt, and they are not
interchangeable:
| Field | What it marks |
|---|---|
receivedAt | The instant the submission was accepted and staged |
startedAt | When work on it was first picked up. Null while queued |
completedAt | When all of its work settled. Null until then |
receivedAt cannot stand in for startedAt. The gap between them is queue wait, not time
spent working, so processing time is completedAt - startedAt and end-to-end latency is
completedAt - receivedAt. A submission that started no work at all keeps both startedAt and completedAt null
forever, even though its status still reaches a terminal value.
One batch per patient at a time
A patient's submissions are processed one batch at a time, in order. A submission that arrives while that patient still has work in flight is deferred, not refused — it is staged and receipted exactly as usual, and started on its own once the work ahead of it finishes.
Two consequences worth designing around:
startedAtcan lagreceivedAtby a long way on a busy patient. That is queue wait, not a stall.- A later submission never overtakes an earlier one for the same patient. Sending corrections in order means they are applied in order.
Several submissions that pile up behind the same patient are picked up together rather than one at a time, so a backlog drains in batches rather than serially. Nothing about this is visible in the API beyond the timestamps — you do not schedule it, and there is nothing to configure.
What partial means
The work behind a submission is several steps that proceed independently, and they can end
differently from one another. That is what partial reports. The possible values
of status are:
pending- Accepted, work not startedprocessing- In flightsucceeded- Everything landed and is queryablepartial- The work finished, but part of what you sent is missing from the recordfailed- The work stopped before finishing;failuresays why
A partial submission is a real, terminal outcome. What landed is correct and queryable,
and nothing further will happen to the submission on its own. What is missing will not arrive
later, and re-sending the same content will not bring it — see
Re-sending does not retry.
Failures, and what you can do
The engine retries on its own. A transient failure, a provider timeout, a lost worker, a
restart mid-flight is retried, and a submission whose work was interrupted is detected and
resumed automatically without changing its ingestionId. Because of that, a submission sitting
in processing is usually not stuck, and resending it is rarely the right move.
That automatic recovery stops at a terminal status — a failed submission is never retried on its
own. And once a submission reaches partial or failed it is finished, so re-sending what it
carried does not restart it: that content is already stored, so a re-send stages nothing and
creates no work.
| Status | What to do |
|---|---|
failed | Read failure.code and failure.message. Re-sending the same content will not retry it |
partial | What landed is good. The missing part cannot be recovered by re-sending |
succeeded | Nothing to do |
Re-sending only does something for content the engine has not already stored — new records, or a payload that is partly new.
A way to retry is coming
An endpoint for retrying a submission's unfinished work is designed but not yet available. Until
it ships, content held by a partial or failed submission cannot be reprocessed through the
API.
Re-sending does not retry
There is no idempotency key. Deduplication is content-based: re-sending a document the engine
has already stored stages nothing, and its receipt comes back with every staged counter at
zero. Sending the same bundle twice does not store it twice, whatever request id it arrives
under.
The trap is what that second receipt then reports:
A re-send that did nothing still settles as succeeded
A re-delivery that staged nothing gets its own ingestionId, and that receipt settles as
succeeded — it created no work, so it has nothing left to do. That says nothing about the
content you sent, which may still be sitting in an earlier submission that is partial or
failed. A poller that reads it as confirmation will treat unprocessed content as healthy.
So to find out where content stands, check the submission that first carried it — the one whose
staged counters were non-zero — rather than the receipt from a re-send. staged says what
this call contributed, and status says where this call's work got to.
When a submission created nothing, intake distinguishes the two reasons. Entries reading
ignored mean nothing in the payload could be used; anything else means the content was already
stored by an earlier submission.
Every submission appears in one place
GET /v1/patients/{patientId}/ingestions lists a patient's submissions regardless of how they
arrived, which makes it the one place to ask whether a patient's data has settled and filter it
rather than tracking receipts individually:
curl "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions?status=pending,processing&perPage=1" \
-H "Authorization: Bearer <access-token>"meta.total is how many submissions are still outstanding, and zero means this patient has
settled.
Some entries in that list predate the ingestions API. Every submission made before it existed was
backfilled so that history stays readable, and those entries carry no accounting: intake is
absent and every staged counter is 0, because none of that history was measured at the time.
Two consequences for anything that reads the list:
intakeis optional. Code that reads it unconditionally will trip over your own oldest records.sourcecarries values no current route produces —session/v1,batch/legacyanddocument/legacyappear only on backfilled history. Treatsourceas an open vocabulary rather than switching exhaustively on the three you can currently send.
What "settled" makes true
Once a submission reaches succeeded, everything it contributed is visible everywhere the
patient's record is served: the virtual file system,
condition stories, search, and the graph queries.
Worth knowing: each submission is resolved against everything already held for that patient, not just against itself. A medication ingested today is linked to a condition ingested last year, and a document that duplicates an existing record collapses into it rather than sitting beside it. That is why the record improves as more sources arrive, and why ingesting the same patient from a second system is additive rather than duplicative. See Entity Resolution.