The Context Engine Studio is now open source.  View on GitHub →
Clinia
How-to Guides

Ingest a FHIR Bundle

POST a FHIR R4 or R5 Bundle, poll the receipt it returns, and read what the submission could not use.

Ingest a FHIR Bundle

Send a FHIR Bundle (or any single FHIR resource) to register it against a patient. The patientId in the URL is your registry key — the stable identifier used in all subsequent calls for this patient.

The FHIR release is part of the path, because a Bundle does not state which release it is written against. Post R4 payloads to /ingestions/fhir/r4 and R5 payloads to /ingestions/fhir/r5.

Make the request

Create the patient before the first ingest:

curl -X PUT "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}" \
  -H "Authorization: Bearer <access-token>"

This call is idempotent, so it's safe to run before or after your first ingest.

curl -X POST "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions/fhir/r4" \
  -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" \
  -d @fhir-bundle.json

A successful call answers 202 with a receipt:

{
  "ingestionId": "c1m2k3n4p5q6r7s8t9u0v1w2",
  "source": "fhir/r4",
  "mimeType": "application/json",
  "receivedAt": "2026-09-10T14:22:31.004Z",
  "status": "pending",
  "staged": { "facts": 12, "events": 8, "narratives": 1 },
  "intake": [
    {
      "unit": "resource",
      "received": 42,
      "entries": [
        { "kind": "Condition", "count": 4, "outcome": "structured" },
        { "kind": "Observation", "count": 30, "outcome": "structured" },
        { "kind": "DocumentReference", "count": 1, "outcome": "text_only" },
        { "kind": "ImagingStudy", "count": 7, "outcome": "ignored", "reason": "unsupported" }
      ]
    }
  ],
  "startedAt": null,
  "completedAt": null,
  "failure": null
}

202 means accepted, not finished. Extraction continues after the response, so nothing is queryable when the call returns. ingestionId is the only handle you need to keep — poll it to follow the work. See Ingestion for what happens between acceptance and a settled submission.

startedAt is null until the work is picked up — the gap from receivedAt is queue wait, not time spent processing.

staged counts the rows this submission actually contributed. Re-sending content that is already stored stages nothing and reports zeros, which is how a duplicate delivery is distinguished from a new one.

Request details

The body must be a valid FHIR JSON document — either a Bundle or a single resource such as Patient, Condition or MedicationRequest — and Content-Type must be application/json.

Supported resource types include: Patient, Condition, MedicationStatement, MedicationRequest, Observation, AllergyIntolerance, Coverage, Immunization, Procedure, Encounter, FamilyMemberHistory, Device, DeviceUseStatement, DeviceUsage, and DiagnosticReport.

Each call is one submission and one receipt. Multiple calls for the same patientId accumulate sources, and entity resolution re-runs over everything accumulated.

Clinical prose. A DocumentReference contributes no entities. Its text attachment becomes a narrative instead — kept verbatim, readable under /sources/, and adjudicated against the facts it speaks to. The attachment must be inline (data) or a Binary in the same bundle, and one of text/plain, text/html or application/xhtml+xml. A remote url, or any other content type, appears in intake as text_only turning to ignored — so a bundle can carry documents and contribute none of their prose.

Error responses

Validation is synchronous: a malformed payload is rejected before anything is staged.

StatusCause
400Request body is not valid JSON or is missing a resourceType field
404No such patient
422Bundle contains no extractable FHIR resources

Follow the submission

Poll the receipt until it settles:

curl "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions/{ingestionId}" \
  -H "Authorization: Bearer <access-token>"

status says where the submission stands across all its work. The possible values are:

  • pending - Accepted, not started
  • processing - In flight
  • succeeded - Everything landed and is searchable
  • partial - Searchable, but some of what you sent is missing
  • failed - The work stopped before finishing; read failure for the reason

To ask whether a patient's data has settled rather than watching one submission, filter the listing:

curl "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions?status=pending,processing" \
  -H "Authorization: Bearer <access-token>"

meta.total is the number of submissions still outstanding. Zero means this patient has settled.

Read what was not used

intake accounts for every resource you sent, grouped by kind and outcome, so a submission never silently discards part of itself. It is fixed when the request is accepted and never recomputed.

Each entry's outcome says what was done with resources of that kind. The possible values are:

  • structured - Clinical detail was read from it
  • text_only - Kept, but as prose — no structured detail was read
  • ignored - Not used at all; reason says why

An ignored entry carries a reason, and the reasons are differently actionable. The possible values are:

  • unsupported - The type is not modelled yet — nothing to fix, it may be used later
  • unusable - Recognised, but missing what it needs (no code, no text)
  • empty - Recognised and well-formed, but carries no extractable content

For the resolved picture across every source, use the reconciliation summary.

Submitting several documents

Each call carries one Bundle, and a Bundle is already a collection — put several resources in one and post it once. For several independent Bundles, make several calls. How that work is divided internally is decided for you, so nothing about the grouping needs to be expressed in the request.

On this page