Docs · FHIR Mapping
FHIR data mapping: from CSV to FHIR-ready fields
Most healthcare data does not arrive as FHIR. It arrives as a CSV export, an Excel sheet, or a legacy table — in five languages, with free-text headers and inconsistent code systems. FHIR mapping is the work of turning those columns into the resources, fields, and terminology a FHIR-aware system expects.
Concepts
What FHIR mapping is
FHIR (Fast Healthcare Interoperability Resources, an HL7 standard) models healthcare data as resources — for example a Patient, an Observation, a Medication, or a Claim. Each resource has defined fields (elements) with types and cardinality, and many fields carry terminology bindings — they expect a coded value drawn from a specific code system rather than free text.
FHIR mapping, then, is a three-part problem: figure out which FHIR resource a row of your data represents, line up each of your columns with the right field on that resource, and make sure coded fields carry values from the right terminology — LOINC for lab observations, ICD-10 for diagnoses, CPT for procedures, and so on. The standard spells out the resources and their fields in detail at hl7.org/fhir.
The hard part
Why CSV → FHIR is hard
The gap between a spreadsheet and a FHIR resource is wider than it looks. Three things make it hard:
- Free-text headers. A column called
DOB,birth_dtorPatient Birthdayall meanPatient.birthDate, but no two source systems agree on the spelling. - Locale variance. Headers, date formats and gender values differ across German, French, Italian, English and Spanish source systems — common in multi-region healthcare data.
- Code systems. FHIR’s coded fields expect values from specific terminologies — LOINC, ICD-10, CPT, ATC, NPI — and a raw export rarely arrives validated against them.
Get any of these wrong and the resulting resource is either rejected by the receiving system or — worse, for your-money-or-your-life healthcare data — silently incorrect.
The approach
How AdaptivMapr maps to FHIR
AdaptivMapr maps your inbound columns to a template’s canonical fields, and each healthcare template carries a fhir_resource mapping so you always know which FHIR resource and path a field corresponds to. It resolves and validates the mapping layer; your pipeline serialises the FHIR-ready fields into resources.
The five-layer cascade
Header resolution runs through five layers, cheapest first. When a layer accepts a column, the layers below it never run — and the metered LLM is the last resort, not the first move.
- 1 · Statistics — no AI cost, deterministic; auto-accepts headers seen mapped the same way before.
- 2 · Heuristic — no AI cost; compares the normalised header against the column name, label and every multilingual hint (DE / FR / IT / EN / ES).
- 3 · Fuzzy — no AI cost, pure compute; tolerates typos and reordered tokens.
- 4 · Semantic — cheap, cached embeddings for the long tail of paraphrases.
- 5 · LLM — metered; fires only on genuinely ambiguous columns, constrained to the template’s allowed field set so it cannot invent a field.
Validators & HITL gating
Healthcare templates attach field-level validators — LOINC, ICD-10, CPT, ATC, NPI, GTIN, IBAN and more — that run on every committed row. Each template also carries a risk level. Medium- and high-risk templates — patient_demographics_v1, lab_results_v1, claims_line_items_v1 and drug_formulary_v1 — return requires_hitl: true with hitl_status: "pending_review" so you can gate your own commit workflow with a human review step.
On data handling: schema-only mapping is the default data-minimization mode — only headers and up to three sample rows (≤80 characters each) leave your environment. Full-data mapping requires a PHI entitlement, where the layer-5 LLM call is routed through a PHI-eligible provider with X-PHI and X-Region headers to force a PHI-eligible, in-region model. A BAA is accepted in-app before any PHI-routed run, and AdaptivMapr is HIPAA-ready — HIPAA is not a certification anyone can hold.
Output
Ask for a bundle, get a bundle.
The FHIR emit path is one field on the call you were already making. It serialises the accepted rows into the resource the template declares.
- POST
/v1/uploads/:id/commitoutput: "fhir"returns abundleinline, or one{batch_index, bundle}per signed webhook batch.commit - POST
/v1/transformThe sameoutputfield on the one-shot route — upload, map, validate and serialize in a single call.transform - GET
/v1/templates/:idRead a template’sfhir_resourceand its field-level validators before you map anything.public
POST /v1/uploads/upl_8f3a92c1/commit
{ "skip_invalid_rows": true, "output": "fhir" }{
"committed": true,
"accepted": 1238,
"skipped": 6,
"output": "fhir",
"fhir_resource": "Patient",
"bundle": { "resourceType": "Bundle", "type": "collection",
"entry": [ /* one entry per accepted row */ ] }
}The fields that matter on that call
| Field | Type | What it does | |
|---|---|---|---|
output | "fhir" | required for a bundle | Serialises the accepted rows into the resource the template declares. Without it the commit returns plain rows. |
skip_invalid_rows | boolean | default false | A bundle built from rows that failed their validators is a bundle the receiving system will reject. Either skip them and read the count, or fix them and re-commit. |
webhook | { url, secret } | optional | Delivers one {batch_index, bundle} envelope per 500 rows instead of a single inline bundle, with x-mapr-fhir-resource on each request. |
Everything else on POST /v1/uploads/:id/commit behaves exactly as it does for a rows commit — see the quickstart for the full body.
Which templates can emit a bundle. The 8 templates that declare a fhir_resource today:
PatientPatient demographicsmediumObservationLab result cataloglowMedicationDrug formularymediumClaim.itemClaims line itemsmediumAppointmentAppointment loglowCoverageInsurance contractslowPractitionerProvider directorylowObservationLab resultsmedium
Read from the catalogue at build time, so this list is whatever the product actually ships.
Worked examples
Three columns, mapped
Each example links to the live template page, where you can see the full canonical schema, its validators and the FHIR resource mapping.
"Geburtsdatum" → date_of_birth
template : patient_demographics_v1
fhir : Patient.birthDate
caught by : Layer 2 · HeuristicThe German header matches a registered hint after the normalising pass, so the column resolves without AI. The date_range validator (1900-01-01 to today) fires on commit.
Patient demographics template"code loinc" → loinc_code
template : lab_result_catalog_v1
fhir : Observation.code
caught by : Layer 2 · HeuristicThe French label resolves against the loinc hint, and the loinc_code validator checks the value against LOINC’s code format on commit.
Lab result catalog template"Diagnose (ICD)" → icd10_code
template : claims_line_items_v1
fhir : Claim.item
caught by : Layer 3 · FuzzyA messy header with extra tokens still resolves via the fuzzy layer. Because this template is risk medium, the mapping is returned with requires_hitl:true so your team can gate the commit.
Claims line items templateQuestions
FHIR mapping FAQ
Get started
Map your first file — without shipping raw records.
Schema-only mapping keeps raw records on your side — start there. The documentation covers the /v1 reference, the MCP server, and the template catalogue.