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

Ingest a CDA Document

POST one or several CDA R2 XML documents, poll the receipt, and handle common parsing gotchas.

Ingest a CDA Document

Send a CDA or C-CDA R2 XML document 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.

Make the request

Post one document as the request body, under application/cda+xml:

curl -X POST "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions/cda/r2" \
  -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/cda+xml" \
  --data-binary @summary.xml

Use --data-binary rather than -d to preserve XML whitespace and encoding declarations.

A successful call answers 202 with a receipt:

{
  "ingestionId": "c1m2k3n4p5q6r7s8t9u0v1w2",
  "source": "cda/r2",
  "mimeType": "application/cda+xml",
  "receivedAt": "2026-09-10T14:22:31.004Z",
  "status": "pending",
  "staged": { "facts": 9, "events": 4, "narratives": 3 },
  "intake": [
    {
      "unit": "section",
      "received": 12,
      "entries": [
        { "kind": "10160-0", "label": "Medications", "count": 1, "outcome": "structured" },
        { "kind": "11348-0", "label": "Past Medical History", "count": 1, "outcome": "structured" },
        { "kind": "8648-8", "label": "Hospital Course", "count": 1, "outcome": "text_only" }
      ]
    }
  ],
  "startedAt": null,
  "completedAt": null,
  "failure": null
}

202 means accepted, not finished. Ingestion 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.

Several documents in one call

CDA documents are independently rooted XML, so several arrive as multipart/form-data with one documents part per document:

curl -X POST "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions/cda/r2" \
  -H "Authorization: Bearer <access-token>" \
  -F "documents=@summary.xml;type=application/cda+xml" \
  -F "documents=@discharge.xml;type=application/cda+xml"

One call is one submission and one receipt, whatever the document count. intake comes back index-aligned with the documents you sent, so which document held what stays answerable from a single receipt.

All or nothing. If any document cannot be parsed the whole request is rejected and nothing is stored.

Request details

The body must be raw CDA/C-CDA XML. Do not JSON-encode it.

OID-to-URI code system mapping. CDA documents use OIDs for code systems. The extractor maps the common OIDs automatically:

OIDSystem
2.16.840.1.113883.6.96SNOMED CT
2.16.840.1.113883.6.90ICD-10-CM
2.16.840.1.113883.6.3ICD-10
2.16.840.1.113883.6.88RxNorm
2.16.840.1.113883.6.1LOINC
2.16.840.1.113883.6.69NDC
2.16.840.1.113883.6.12CPT
2.16.840.1.113883.12.292CVX

An OID the extractor does not map is carried as a urn:oid: URI. Entity resolution recognizes more code systems than the table above, and resolves a coding's system before comparing it, so many of these still match on codes in layer 1 — an ATC or UCUM coding among them. One that no layer can place still resolves via NLP normalization or embedding similarity in layers 2–3. Each unresolved entity contributes a warning to the ingest response.

Clinical prose. A section's coded entries are not all it holds. Its narrative text is kept verbatim, readable under /sources/, and becomes a narrative adjudicated against the facts it speaks to — with prose that recurs across sections and documents grouped into one narrative rather than repeated.

Not every section's text is taken. A section the extractor recognizes and that carries coded entries has its prose held back, because that prose is typically a machine rendering of the entries already ingested and lifting it would let the record corroborate itself. A recognized section carrying no entries — the narrative-only shape common in discharge summaries — is taken.

Section in the documentIs its prose taken?intake outcome
Medications, carrying coded entriesNo. The prose restates entries already ingestedstructured
Medications, carrying no entriesYestext_only
Hospital Course, not a structured kindYestext_only

A section read as prose is reported as text_only rather than as an error: nothing was lost, it took a different path. That distinction is the reason intake has three outcomes rather than two.

Encoding. CDA files are commonly encoded as Windows-1252 or ISO-8859-1. If the XML declaration declares a non-UTF-8 encoding, convert the file before sending:

iconv -f windows-1252 -t utf-8 summary.xml | \
  curl -X POST "https://<workspace-id>.w.clinia.cloud/v1/patients/{patientId}/ingestions/cda/r2" \
    -H "Authorization: Bearer <access-token>" \
    -H "Content-Type: application/cda+xml" \
    --data-binary @-

Error responses

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

StatusCause
400A document is missing, or the body is not recognized as CDA XML
404No such patient
422XML could not be parsed (malformed structure or missing required CDA header elements)

On a 422, check that the document has a valid ClinicalDocument root element and a structuredBody or nonXMLBody.

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 moves through pending → processing → one of succeeded, partial (searchable, but some of what you sent is missing) or failed (the work stopped before finishing; read failure).

To ask whether a patient's data has settled rather than watching one submission, filter the listing — meta.total is the outstanding count, and zero means settled:

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

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

On this page