Use case · Healthcare
Move a patient list off the old EHR and out as FHIR Patient.
A German-labelled demographics export lands on patient_demographics_v1, gets birth dates and identifiers validated per row, and emits a FHIR R4 Patient bundle — with the rows themselves never leaving your side in schema-only mode.
The job
What you are actually trying to get done
You are replacing a practice-management system and the vendor hands you an Excel export. The receiving system wants FHIR R4 Patient resources; the export has German column labels, Swiss date formatting and an AHV number where you expect an identifier. The job is not “write a parser” — it is to get every row into the new system with the identifiers still valid and an audit trail of who approved the mapping.
Where it lands
Patient demographics
patient_demographics_v1 · risk: medium · FHIR Patient
The template declares fhir_resource: "Patient", so the same rows can come back as a FHIR Bundle from output: "fhir" — shaped by lib/fhir.ts, the same code the commit path uses, so a row emits identical FHIR whichever route produced it.
Canonical columns
- first_name
- last_name
- date_of_birth
- gender
- ahv_number
- phone
7 canonical columns · 3 required · 4 validated
Step by step
The whole run, one call at a time
- 1
Upload the workbook
Multipart, up to 10 MiB. The response names every worksheet insheetsand which one it read — re-POST withsheet_nameto pick another. Sample rows come back already clamped to three rows of ≤80 characters.POST /v1/uploads - 2
Run the cascade
Nachname,Vorname,GeburtsdatumandGeschlechtare all hint entries on the template’s fields, so they resolve on layer 2 at 1.00 with no model, no key and no network call. The response reports which layer settled each column.POST /v1/uploads/:id/match - 3
Fix what you disagree with
Your override is recorded and feedsmapping_statistics, so the same header resolves on layer 1 next quarter. The same response carriesrequires_hitl: true, because this template is medium-risk.PATCH /v1/uploads/:id/mappings - 4
Validate every row
date_of_birthis checked against adate_rangeof 1900-01-01 → today;ahv_numberagainst the Swiss756.dddd.dddd.ddpattern;emailandphoneby their own validators. Errors come back per row, capped at 500.POST /v1/uploads/:id/validate - 5
Commit as FHIR
output: "fhir"returns a Bundle inline, or in HMAC-signed batches of 500 to your webhook. The confirmed mapping is recorded inmapping_layouts, so the next export from the same system is a one-click reuse.POST /v1/uploads/:id/commit
In code
Four German headers, no AI, no key
POST /v1/match is public and unauthenticated, so you can check the mapping before you have an account. Send headers plus up to three sample rows; the response names the layer that resolved each column.
curl https://api.adaptivmapr.com/v1/match \
-H "Content-Type: application/json" \
-d '{
"template_id": "patient_demographics_v1",
"headers": ["Nachname", "Vorname", "Geburtsdatum", "Geschlecht"],
"sample_rows": [["Lovelace", "Ada", "10.12.1815", "W"]]
}'{
"template_id": "patient_demographics_v1",
"matches": [
{ "source_col": "Nachname", "target_field": "last_name", "source": "heuristic", "confidence": 1 },
{ "source_col": "Vorname", "target_field": "first_name", "source": "heuristic", "confidence": 1 },
{ "source_col": "Geburtsdatum", "target_field": "date_of_birth", "source": "heuristic", "confidence": 1 },
{ "source_col": "Geschlecht", "target_field": "gender", "source": "heuristic", "confidence": 1 }
],
"auto_accept_threshold": [{ "minN": 100, "minRatio": 0.95 }, { "minN": 20, "minRatio": 1 }],
"cascade_layers": ["statistics", "heuristic", "fuzzy", "ranker", "semantic", "ai"],
"unmapped": []
}- In schema-only mode: your headers and at most three sample rows, each cell clamped to 80 characters.
clampForSchemaOnly()inlib/parser.tsis the single chokepoint, re-applied at the HTTP edge in every route that takes sample rows. - The
Geschlechtfield carries adescriptionthe model reads when it gets that far —M/Männlich/Homme → male. On this file it never gets that far, because layer 2 already claimed the column. - Full-data mode (row-level AI cleanup) is gated on the PHI entitlement and returns
402 phi_gateway_requiredbefore any data leaves if the workspace does not hold it. - Uploads live in Cloudflare KV under your workspace's retention window —
24hby default, up to30d, or destroyed at commit — and are mirrored to Supabase under an explicittenant_id. Deleting the workspace destroys both — one implementation,lib/workspaceErasure.ts.
Where the work lands
Which layer resolves this file
| Layer | What it does here | Auto-accepts at | Cost |
|---|---|---|---|
| 1 · statistics | Header→field counts your workspace already confirmed on earlier exports from this vendor | ≥100 @ 95% · ≥20 @ 100% | Free · deterministic |
| 2 · heuristic | “Nachname”, “Vorname”, “Geburtsdatum”, “Geschlecht” — all shipped hints on the template | ≥ 0.85 | Free · deterministic |
| 3 · fuzzy | “AHV-Nr.” against the ahv_number field after punctuation is stripped | ≥ 0.80 | Free · pure compute |
| 4 · semantic | A house label with no shared vocabulary — “Pat.-Kennung”, “Stammdaten-ID” | ≥ 0.78 | Cheap, cached · off without an embedding key |
| 5 · ai | Whatever is genuinely ambiguous, resolved in ONE batched call over the leftovers | Model pick, constrained to the unclaimed column set | Metered · the only paid layer |
MAPR_EMBEDDING_API_KEY, MAPR_LLM_API_KEY) and fail soft to OFF, so an unconfigured deployment still maps on the deterministic layers rather than erroring.Before you commit
Review, cost and where it routes
It comes back flagged for review
patient_demographics_v1 is a medium-risk template, so PATCH /v1/uploads/:id/mappings returns requires_hitl: true and hitl_status: "pending_review" — automatically, from the template’s own risk field.
Honest limitThe flag is advisory in v1: it tells your importer to gate, it does not block the commit server-side. Native AgentGate queue integration is roadmap Q3 2026.
Usually just the flat fee
When every column resolves on layers 1–3 — the common case for a file you receive regularly — no model runs, so there are no AI tokens to bill. A small flat per-map fee ($0.0010) is drawn on every map — a fully deterministic one and a layout-cache re-map included. There is no free tier and no seats or contract; you top up a prepaid wallet from $10 and it draws down.
Rate cardAI tokens are billed only when a model actually ran, at provider cost ×2 (×0.5 with your own LLM key). A PHI-routed run multiplies the whole charge — flat fee included — by 1.2. See lib/pricing.ts and GET /v1/pricing.
PHI routing is opt-in, and gated
Standard is what an unconfigured workspace gets. Switch a run to PHI · enterprise and the layer-5 call goes to phi-cloud with X-PHI and a region pin, so a PHI-eligible in-region model handles it under the BAA.
GateLocked until the workspace accepts the BAA in-app (Settings → Security & Data, POST /v1/me/agreements). An explicit PHI ask without one is 403 agreement_required with a settings_url pointer — never a silent downgrade. PHI adds 20% to the whole map charge.
Questions
The things people actually ask
Do patient rows leave our systems?
Is AdaptivMapr HIPAA certified?
Does this replace Redox or an integration engine?
Which FHIR resources can it emit?
What happens the second time this vendor sends a file?
Stop hand-mapping this file. Map it once.
Start with a $10 prepaid wallet. In schema-only mode only headers and up to three sample rows — each cell clamped to 80 characters — ever leave you.