AdaptivMapr

Solution · Healthcare

The lab file lands as FHIR R4.

An EHR export, a lab CSV and a payer file all arrive in different shapes. They land in your schema — or as FHIR R4 resources — without the records leaving your control.

R4 resource types
14
Rows, in schema-only
≤ 3
PHI, under a BAA
+20%
The pack

Healthcare

templates
10templates
gate on review
4gate on review
FHIR R4 resources
7FHIR R4 resources
hint languages
5hint languages
employee_roster_v1patient_demographics_v1lab_result_catalog_v1drug_formulary_v1claims_line_items_v1+5 more

The job

Three systems, three shapes, one patient.

The EHR writes Geburtsdatum. The lab sends LOINC codes in a column called Analyse. The payer’s file uses CPT and its own member ids. Every one of them is the same patient, and every one of them needs a person to sit with the file and work out which column is which — on data that cannot casually be pasted into a chat window to ask.

What changes

The columns resolve against a healthcare template, the medical codes are checked rather than trusted, and the result comes back as rows or as FHIR R4 resources. In schema-only mode the records themselves never leave.

What actually arrives

Every source exports it differently.

One export per source system, none of them alike: an EHR writes Geburtsdatum, a lab writes LOINC, a payer writes CPT. Mapping each to FHIR R4 by hand is slow and error-prone — and it is exactly the data you least want to hand to a generic model. Schema-only mapping never lets a record leave.

Four shapes cover most of what arrives. None of them is wrong — they are each correct for the system that wrote them, and each one is a different mapping problem.

EHR / practice-management export

CSV or XLSX, one row per patient

Header row in German, French or Italian; dates as DD.MM.YYYY; gender as M / W / F or 1 / 2; the AHV number formatted 756.xxxx.xxxx.xx in one system and unformatted in the next.

LIS lab result file

CSV, one row per result

LOINC present, absent, or hidden in a column called Analyse; the unit baked into the header — “Hb (g/L)” — instead of its own column; the collection timestamp named Entnahmedatum, Prélèvement or just Datum.

Payer claims extract

CSV or fixed-width, one row per line item

CPT and ICD-10 in adjacent, similarly named columns; amounts with a comma decimal separator; service dates in a second format from the same vendor’s other feed.

Formulary or supplier catalogue

XLSX, often with a banner row above the headers

ATC, GTIN and Pharmacode all present but under vendor SKU names; the real header row sitting three rows down under a title and a logo.

10 templates

Healthcare — every field carries DE / FR / IT / EN / ES hints.

4 gate on review

4 medium + 0 high — PATCH /mappings returns requires_hitl: true.

7 FHIR R4 resources

Patient, Observation, Medication, Claim.item, Appointment, Coverage, Practitioner

  • Headers + ≤3 rows, 80 chars each
  • Layers 1–3 need no model
  • requires_hitl on medium + high
  • PHI pinned in-region under a BAA
  • Per-workspace, never cross-tenant

How it works

Four calls, and the file is in your schema.

Upload, cascade, confirm, commit. The deterministic layers do most of the work before anything metered runs, and nothing but headers and a few clamped sample rows leaves your system on the way.

  1. 1Step 1

    Upload the clinical or claims export

    A CSV or Excel file straight out of your EHR, LIS or claims system. The response gives you the detected columns and up to three sample rows, each clamped to 80 characters — that clamp is the whole of what schema-only sends.

    POST /v1/uploadscsv · xlsx · xlsmschema-only
  2. 2Step 2

    The cascade resolves the headers

    Six layers, cheapest first. Statistics, heuristic and fuzzy are pure compute and cost nothing; DE / FR / IT / EN / ES hints on every field mean Geburtsdatum, date de naissance and fecha de nacimiento all land on date_of_birth. Only what falls through reaches a metered model.

    statisticsheuristicfuzzysemanticai
  3. 3Step 3

    Validators check the medical codes

    Field-level validators run before a value is accepted: loinc_code on lab codes, icd10_code on diagnoses, atc_code and gtin on the formulary, cpt_code on claim lines, npi and gln on providers. A malformed code fails at map time with its own error code, not on ingest downstream.

    loinc_codeicd10_codeatc_codecpt_codenpi
  4. 4Step 4

    Emit FHIR R4, or deliver rows

    Commit emits FHIR R4 from the template’s fhir_resource mapping — Patient, Observation, Medication, Claim.item, Appointment, Coverage, Practitioner — or writes the mapped rows to your warehouse, a signed webhook or a saved database connector instead.

    PatientObservationClaim.itemPractitioner

Field level

One template, printed in full.

This is not a description of the template — it is the template. Every row below is a field object the catalogue actually ships, with the validator ids that run on it and the header vocabulary the free heuristic layer already recognises.

lab_results_v1mediumFHIR R4 · Observation5 fields · 4 required · 1 validated
FieldTypeRequiredValidatorsHeader vocabulary it already knows
patient_idstringrequired—patient · patient_id · pid
loinc_codestringrequiredloinc_codeloinc
valuenumberrequired—wert · valeur · valor
unitstring——einheit · unité · unidad
taken_atdaterequired—entnahme · prélèvement · fecha
Every row is a field object from GET /v1/templates/lab_results_v1 — not a description of one. The hint lists are what the heuristic layer compares a header against after normalize() strips accents, punctuation and whitespace, which is why adding a hint is the highest-leverage change in the whole engine: it costs nothing to run and removes a model call.

Why AdaptivMapr

Built for the shape of regulated data.

What the healthcare pack gives you that a generic importer does not — and, under each card, the mechanism that does it.

FHIR is in the template, not bolted on

Eight of the ten healthcare templates carry a fhir_resource mapping, across seven distinct R4 resources, so a confirmed mapping emits FHIR directly. The canonical model is part of the catalogue, not a post-processing step you write.

Howpatient_demographics_v1 → Patient · lab_results_v1 and lab_result_catalog_v1 → Observation · drug_formulary_v1 → Medication · claims_line_items_v1 → Claim.item · appointment_log_v1 → Appointment · insurance_contracts_v1 → Coverage · provider_directory_v1 → Practitioner.

Medical codes are checked, not trusted

Validators run per field at map time, so a transposed LOINC code or an ICD-10 value in a CPT column is caught before anything is committed — with a specific error code you can route on.

Howloinc_code, icd10_code, atc_code, cpt_code, npi (Luhn), gln and gtin (GS1 checksum). SNOMED is deliberately absent: it needs a SNOMED International Affiliate License and no template uses it.

PHI-ready, and HIPAA-ready by posture

Schema-only ships no records at all. When you need full-data mapping, PHI routing is an explicit opt-in that pins the run in-region and is locked until your workspace accepts the BAA in the app. We offer a BAA and hold a HIPAA security risk assessment; SOC 2 is in progress.

GateAn explicit PHI ask without an acceptance returns 403 agreement_required with a settings_url — never a silent downgrade. PHI-routed runs carry a +20% surcharge on the whole map charge.

A German lab file and a French one map the same

Every field carries hints in five languages, and the heuristic layer compares against them at zero cost — so vocabulary drift across sites and vendors is absorbed by the free layers instead of billed to a model.

Howdate_of_birth alone carries geburtsdatum, gebdatum, date de naissance, data di nascita, fecha de nacimiento, dob, birthday and birth date. normalize() strips accents and punctuation before comparing.

The frame

What applies here, and what we actually do about it.

Each frame below is paired with a mechanism, not an adjective. Where we hold nothing, the row says so — a compliance page that overstates is worse than one that is short.

HIPAA (US)

PHI may only be handled by a business associate under a signed BAA, with access controls, an audit trail and a documented risk assessment.

Our posture · We offer a BAA and hold a HIPAA security risk assessment. The product is HIPAA-ready by posture — HIPAA is not a certification any vendor can hold, and we will not say otherwise. Full-data PHI routing is locked until your workspace accepts the BAA in Settings → Security & Data; an explicit PHI ask without one returns 403 agreement_required.

GDPR / nFADP — data minimisation

Art. 5(1)(c): process the minimum personal data necessary for the purpose. A processor that ingests whole clinical files to infer a mapping is doing more than the purpose requires.

Our posture · Schema-only is the minimisation mode: headers plus up to three sample rows, each clamped to 80 characters, and nothing else. One chokepoint — clampForSchemaOnly() in lib/parser.ts — enforces it at the HTTP edge of every route that accepts sample rows. A DPA is provided on request.

Residency

Clinical data frequently may not leave a jurisdiction, and “usually in-region” is not a control.

Our posture · A run carries an explicit region pin, on standard routing as well as PHI. The execution sandbox refuses a region-less run rather than picking one. PHI routing adds X-PHI so phi-cloud selects a residency-locked, BAA-eligible model — the jurisdiction rule is enforced at the gateway that holds the agreement, not asserted here.

FHIR R4 (a standard, not a regulation)

Downstream systems expect canonical resources, not your column names.

Our posture · Eight of the ten healthcare templates carry an fhir_resource mapping across seven distinct R4 resources, and all seven are implemented on the emit path — so commit with output: "fhir" returns a Bundle rather than rows you still have to shape.

What we do not claim: AdaptivMapr is not HIPAA certified (no such certificate exists), SOC 2 is in progress, and this is not a clinical integration engine — we do not replace Redox or an interface engine, and we are not a hosted onboarding UI in place of Flatfile. AdaptivMapr is the mapping layer: the cascade, the template catalogue, the validators and the delivery, under your own product.

Risk & review

The pack, printed.

Every template this vertical ships, with the risk level that decides whether a commit gates on human review, and the FHIR R4 resource it emits.

Template idRiskCommit gateFHIR R4 resource
employee_roster_v1lowrequires_hitl: false—
patient_demographics_v1mediumrequires_hitl: truePatient
lab_result_catalog_v1lowrequires_hitl: falseObservation
drug_formulary_v1mediumrequires_hitl: trueMedication
claims_line_items_v1mediumrequires_hitl: trueClaim.item
appointment_log_v1lowrequires_hitl: falseAppointment
insurance_contracts_v1lowrequires_hitl: falseCoverage
supplier_inventory_v1lowrequires_hitl: false—
provider_directory_v1lowrequires_hitl: falsePractitioner
lab_results_v1mediumrequires_hitl: trueObservation
Read from the live template catalogue — the same ids, risk levels and resource mappings that GET /v1/templates returns. Nothing on this table is typed by hand.

The gate is a property of the template.

There is no setting for it and no per-id list to maintain. A template rated medium or high makes PATCH /mappings return requires_hitl: true and hitl_status: "pending_review" — on every call, for every workspace, derived in one line of the route from the risk field. AdaptivMapr sets the flag; your workflow owns the queue. A native review queue is on the roadmap, not in v1.

4 medium + 0 high

In this vertical, out of 10 templates.

13 flagged of 33

Across all 7 packs the engine serves on GET /v1/templates.

PATCH /v1/uploads/:id/mappings
[
  { "source_col": "Patient",
    "target_field": "patient_id",
    "user_confirmed": true }
]
response · patient_demographics_v1
{
  "upload_id": "upl_7a41c2…",
  "mappings": [ … ],
  "requires_hitl": true,
  "hitl_status": "pending_review"
}
the body is an ARRAY of { source_col, target_field, user_confirmed } · 409 no_template if /match has not run on this upload yet

In code

A real call, end to end.

A German lab export against lab_results_v1. Five headers, five hits on the free heuristic layer, nothing sent to a model — and only the headers plus three clamped sample rows ever left the building.

  • POST/v1/uploadsparse the file, return its headers + ≤3 clamped sample rows
  • POST/v1/uploads/:id/matchrun the six-layer cascade against one template
  • PATCH/v1/uploads/:id/mappingsrecord confirmations; returns requires_hitl
  • POST/v1/uploads/:id/validaterun the field validators over every row, indexed
  • POST/v1/uploads/:id/commitemit and deliver — inline, webhook or DB connector

Authentication is one header. Keys are HMAC-signed and carry their own tenant_id, so verifying one needs no database lookup — and a key can be scoped so the service that reads mappings is not the one allowed to commit them.

curl
# 1 · upload — schema-only: headers + <=3 rows, 80 chars each
curl -s https://api.adaptivmapr.com/v1/uploads \
  -H "Authorization: Bearer $MAPR_KEY" \
  -F file=@laborwerte.csv

# 2 · run the cascade against the FHIR template
curl -s https://api.adaptivmapr.com/v1/uploads/$ID/match \
  -H "Authorization: Bearer $MAPR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_schema":{"schema_id":"lab_results_v1"},
       "mode":"schema-only"}'

# 3 · confirm a column, then commit (medium risk = gated)
curl -s https://api.adaptivmapr.com/v1/uploads/$ID/mappings -X PATCH \
  -H "Authorization: Bearer $MAPR_KEY" \
  -H "Content-Type: application/json" \
  -d '[{"source_col":"Entnahmedatum",
        "target_field":"taken_at","user_confirmed":true}]'
response
{
  "template_id": "lab_results_v1",
  "mode": "schema-only",
  "matches": [
    { "source_col": "PID",           "target_field": "patient_id", "confidence": 1,    "source": "heuristic" },
    { "source_col": "LOINC",         "target_field": "loinc_code", "confidence": 1,    "source": "heuristic" },
    { "source_col": "Wert",          "target_field": "value",      "confidence": 1,    "source": "heuristic" },
    { "source_col": "Einheit",       "target_field": "unit",       "confidence": 1,    "source": "heuristic" },
    { "source_col": "Entnahmedatum", "target_field": "taken_at",   "confidence": 0.85, "source": "heuristic" }
  ],
  "unmapped": [],
  "cascade_layers": ["statistics","heuristic","fuzzy","ranker","semantic","ai"]
}
schema-only · headers + ≤3 rows clamped to 80 chars left your system · medium/high templates gate before commit

Rollout

From one file to a feed that runs itself.

Four phases, none of which requires the previous one to be finished. Most teams stop after the third and only wire the fourth when a customer starts sending the same file every month.

  1. 1

    Map one real export by hand

    Take a file you already received, run it against a healthcare template in schema-only mode, and read the per-column confidence and the layer each match came from. No integration, no schema work, no commitment beyond a $10 wallet.

    POST /v1/uploadsmode: schema-only
  2. 2

    Wire the four calls

    uploads → match → mappings → commit, driven by a scoped API key. Keys are HMAC-signed and carry their own tenant_id, so verification needs no lookup and a key can be scoped to just the verbs your service uses.

    Bearer mp_live_…scopes
  3. 3

    Route the gate into your review queue

    PATCH /mappings returns requires_hitl on every medium and high template. Send that flag to whatever already approves clinical data in your organisation — we set the flag, you own the queue. A native queue is roadmap, not v1.

    requires_hitlhitl_status
  4. 4

    Make the recurring files free of mapping

    Every confirmed mapping is cached whole against a fingerprint of the normalized header row, and every confirmation teaches the statistics layer. The next month’s file from the same site is a lookup, and drift in its columns is a deterministic diff rather than a surprise.

    /v1/layouts/lookup/v1/layouts/driftconnectors

FAQ

The questions that decide it.

Does patient data leave my system to get mapped?
Not in schema-only mode. Only the column headers and up to 3 sample rows — each clamped to 80 characters — are sent, and the cascade maps on those alone. The clamp is enforced at the HTTP edge in every route that accepts sample rows, not left to the client. Full-data mapping is a separate, explicit opt-in.
Which FHIR resources does the pack emit?
Eight of the ten healthcare templates carry a fhir_resource mapping, across seven distinct R4 resources: Patient, Observation (twice — lab_results_v1 and lab_result_catalog_v1), Medication, Claim.item, Appointment, Coverage and Practitioner. You can also skip FHIR entirely and deliver the mapped rows to your warehouse, a signed webhook or a saved database connector.
How are the medical codes actually checked?
By deterministic field validators that run before a value is accepted: loinc_code, icd10_code, atc_code, cpt_code, npi (a Luhn checksum), gln and gtin (GS1 checksums), plus regex and date-range checks. A failure returns its own error code — npi_checksum, gtin_checksum — so you can route on it. SNOMED is intentionally not validated: it requires a SNOMED International Affiliate License and no template uses it.
Why do some templates gate before commit?
patient_demographics_v1, lab_results_v1, claims_line_items_v1 and drug_formulary_v1 are medium-risk, so PATCH /v1/uploads/:id/mappings returns requires_hitl: true and hitl_status: "pending_review". The flag is derived from the template’s risk level — there is nothing to configure. AdaptivMapr sets the flag; your workflow owns the review queue. A native queue is on the roadmap, not in v1.
What does full-data PHI routing actually change?
The cascade still runs in-process. What changes is the layer-5 model call: it goes to phi-cloud with X-PHI and an X-Region pin, so a PHI-eligible, in-region model handles it under the BAA. PHI routing is locked until your workspace accepts the BAA in Settings → Security & Data, and adds 20% to the whole map charge.
Is this a replacement for Redox or an interface engine?
No, and we would not sell it as one. An integration engine moves messages between clinical systems and owns routing, acknowledgements and transport. AdaptivMapr maps a file’s columns to a schema and emits rows or a FHIR Bundle. Teams run it as the mapping layer inside their own pipeline — including pipelines that already have an interface engine in them.
Can we run this with no AI in the loop at all?
Yes. Layers 4 and 5 are config-gated: the semantic layer needs an embeddings key and the LLM layer needs a phi-cloud key, and both fail soft to OFF when the key is absent. With neither configured only the deterministic layers run — statistics, heuristic and fuzzy — and a column that does not resolve comes back in unmapped for a human to assign, rather than being guessed.
What happens when a mapping is wrong?
Every match carries a confidence and the layer it came from, so a low-confidence pick is visible before anything commits. You override it in PATCH /v1/uploads/:id/mappings with user_confirmed: true; that confirmation teaches the statistics layer for your workspace, and when your choice differs from the suggestion the original is recorded as a negative signal so it is less likely to be offered again. Two source columns can never claim the same target field in one run.
What does a clinical import actually cost?
There is no free tier, no seats and no contract. AdaptivMapr runs on a prepaid token wallet with a ~$10 minimum, shared across the phi-cloud suite. Every map draws a small flat fee — a few tokens — including a fully deterministic map and a layout-cache hit that uses no AI. Metered AI work bills the tokens it consumes on top, and a PHI-routed run adds 20% to the whole charge.

Map healthcare data without shipping raw records.

Start on schema-only — headers and a few clamped sample rows, a few tokens per map. Turn on PHI routing when you need it: in-region, under a BAA you accept in the app.

Healthcare data mapping to FHIR R4 — AdaptivMapr — AdaptivMapr