The Context Engine Studio is now open source.  View on GitHub →
Clinia
Concepts

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 4xx instead 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:

FieldWhat it marks
receivedAtThe instant the submission was accepted and staged
startedAtWhen work on it was first picked up. Null while queued
completedAtWhen 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:

  • startedAt can lag receivedAt by 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 started
  • processing - In flight
  • succeeded - Everything landed and is queryable
  • partial - The work finished, but part of what you sent is missing from the record
  • failed - The work stopped before finishing; failure says 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.

StatusWhat to do
failedRead failure.code and failure.message. Re-sending the same content will not retry it
partialWhat landed is good. The missing part cannot be recovered by re-sending
succeededNothing 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:

  • intake is optional. Code that reads it unconditionally will trip over your own oldest records.
  • source carries values no current route produces — session/v1, batch/legacy and document/legacy appear only on backfilled history. Treat source as 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.

On this page