Ingest claim line items with CPT and ICD-10 checked first.
Billing files arrive per clearinghouse, per payer, per month. Map them to claims_line_items_v1, have every CPT and ICD-10 code checked before the batch is written, and emit FHIR Claim.item.
Abbreviated headers, an amount with a currency symbol in it, a service date as 02/14/2026, and an NPI column the target template does not declare.
The job
What you are actually trying to get done
Every payer and every clearinghouse hands you a different line-item layout, and the fields that matter are codes: a wrong CPT is a denial, a wrong ICD-10 is a resubmission. The job is to get each file into one shape, prove the codes are well-formed before you write anything, and keep an auditable record of which mapping produced which batch.
Where it lands
Claims line items
claims_line_items_v1 · risk: medium · FHIR Claim.item
The template declares fhir_resource: "Claim.item" — the line-item half of the two-level Claim split in lib/fhir.ts. Columns the template does not declare (a rendering NPI, a claim number) are not lost: carry them through POST /v1/reshape, or map them to a schema of your own with POST /v1/schemas.
CSV, TSV, Excel, JSON, XML or a SQL dump — all parsed in-process, nothing egresses to read them. 10 MiB ceiling by default (MAPR_MAX_UPLOAD_BYTES).
POST /v1/uploads
2
Map the abbreviations
Proc Code reaches cpt_code through the cpt and procedure hints; Dx1 reaches icd10_code on the fuzzy layer against icd / icd-10. Neither needed a model.
POST /v1/uploads/:id/match
3
Check the codes per row
cpt_code and icd10_code validators run over every row. Errors come back with their row index, capped at 500 per response so a bad batch cannot return a 40 MB error list.
POST /v1/uploads/:id/validate
4
Commit and record the layout
The response carries accepted, skipped and a mapping_fingerprint. Deliver inline (≤10,000 rows), in HMAC-signed webhook batches of 500, or straight into a database through a saved destination connector.
POST /v1/uploads/:id/commit
5
Reuse it next month
Same payer, same header row, same fingerprint — the confirmed mapping comes straight back and the cascade does not run at all. The flat per-map fee still applies; the AI cost does not, because no AI ran.
POST /v1/layouts/lookup
In code
The mappings call, and the flag it returns
Confirming a mapping is also where the review flag appears. It is derived from the template’s own risk field, so there is no per-id list to maintain and no setting to forget.
→ requires_hitl derived from claims_line_items_v1 risk: "medium" · advisory in v1, not a server-side block
A user override is recorded and feeds mapping_statistics, the cascade’s layer 1. Twenty unanimous confirmations of the same header→field pair and it auto-accepts there next time — before any string is compared.
A target field, once assigned, cannot be reused by another header in the same call. Two source columns can never map to one target, and reversing your column order cannot change the result.
Idempotency-Key is NOT read on inbound requests. Retrying a failed commit produces a second map and a second flat fee — build your retry logic around the mapping_fingerprint, not an assumed guarantee.
Webhook batches are signed with X-Mapr-Signature and delivered through a Cloudflare Queue with a dead-letter queue and admin replay. GET /v1/uploads/:id/delivery reports per-batch status.
Where the work lands
Which layer resolves this file
Layer
What it does here
Auto-accepts at
Cost
1 · statistics
This payer’s exact header row, after ~20 unanimous confirmations
≥100 @ 95% · ≥20 @ 100%
Free · deterministic
2 · heuristic
“Proc Code” → cpt_code and “Dx1” → icd10_code via the shipped cpt / icd hints
≥ 0.85
Free · deterministic
3 · fuzzy
“Billed Amt” → amount and “DOS” → service_date after normalization
≥ 0.80
Free · pure compute
4 · semantic
A payer-specific label that shares no vocabulary with the field
≥ 0.78
Cheap, cached · off without an embedding key
5 · ai
One batched call over the leftovers, told which fields are already claimed
Model pick, constrained to the unclaimed column set
Metered · the only paid layer
Layers run in strict cost order and short-circuit — the first layer that resolves a column claims the target field and no later, dearer layer sees it. Layers 4 and 5 are config-gated (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
claims_line_items_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 limit · The 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 subscription; you top up a prepaid wallet from $10 and it draws down.
Rate card · AI 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.
Gate · Locked 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
No. cpt_code is a well-formedness check, not a lookup against a licensed code set. It catches free text, wrong lengths and obviously malformed values. Existence and coverage checking against a specific release belongs in your adjudication logic, where the licence for that code set lives.
On the flat mapping path, an unmapped source column is reported in unmapped and does not appear in the committed rows — nothing is silently invented. If you need it kept, either define your own schema (POST /v1/schemas) that declares it, or use POST /v1/reshape, which supports pass-through columns into the produced sheet. The npi validator exists and is used by provider_directory_v1.
Inline delivery is capped at 10,000 rows (inline_too_large above that). Webhook delivery batches at 500 rows per signed request with no such ceiling, and a database destination writes in batches of 500. Batches are independent — there is no cross-batch transaction — so a non-empty failed_batches means a partial write, and a 200 is not proof of full delivery.
Yes, through a saved destination connector on the commit call — database: { connector_id, table }. sql_write posts INSERT text to a SQL-over-HTTP endpoint; supabase writes JSON rows to PostgREST and is the only destination that supports upsert. Warehouse (BigQuery, Snowflake, Databricks) and SaaS destinations are wired too. Credentials are KEK-encrypted.