Capability · Validators
Catch the bad row before it ships.
A wrong IBAN is not a formatting problem, it is a failed payment. Nineteen checks run before anything is written, and a bad row says exactly what is wrong with it.
- Field validators
- 19
- Not regex shape
- Checksum
The job
The bad row is cheap now and expensive later.
What changes
What you get
Not a demo. The thing that runs every day.
Strict
Real checksums, not regex theatre
A value that looks right but fails its check digit is caught. That is the whole difference between validation and a pattern that makes you feel better.
Howiban is verified mod-97 over the rearranged, letter-expanded string; gtin and gln against the GS1 check digit; npi by Luhn over "80840" + the first 9 digits. All pure compute, no network.
Country-aware
IBAN length by country, on request
The default check is the mod-97 checksum and a shape rule. Turn on strict mode and the length has to match the country too.
How{ type: "iban", strict: true } enforces the per-country length from the SWIFT IBAN Registry (Release 95) — 21 for CH, 22 for DE, 27 for FR. countries narrows the allowed set further.
Clinical
Healthcare code systems
LOINC, ICD-10, ATC and CPT are checked against their format rules, so a lab result or a claim line carries a code that will resolve downstream instead of failing in someone else’s system.
Howloinc_code is NNNNN-N, icd10_code is A00 or A00.0, atc_code is the ATC level pattern, cpt_code is five digits. Format checks, stated as format checks — none of these carry a published check digit we could verify.
Safety
A URL field will not fetch your metadata service
The url validator is not just a shape check. It refuses the addresses a fetched URL should never point at.
Howurl runs checkUrlForSsrfSync() and fails with ssrf_blocked for private ranges and link-local addresses; require_https tightens it to TLS only. A validator on a field that later gets fetched is a security boundary.
One chokepoint
Every path validates identically
The one-shot cascade, the upload commit pipeline and the reshape engine all run the same pass over the same rules. There is no path that writes rows unchecked.
HowvalidateRowsDetailed() in lib/rowValidation.ts is the single implementation. Reshape output used to bypass it; it does not any more.
Fail loud
Flagged, never silently committed
An invalid row comes back with the field, the validator code and the message. And on a risky template, a commit needs a human before it happens at all.
HowMedium- and high-risk templates return requires_hitl: true and hitl_status: "pending_review"; with MAPR_HITL_ENFORCE=1 (production) a commit without hitl_approved: true is refused with 428.
Reference
The nineteen validators
A mapping that finds the right column but writes a malformed value is still a bad import. Every field can carry validators, and they run on the resolved value about to be written — not as a loose regex, but as the real check where a real check exists. A row that fails is flagged or skipped with a specific code, so you always know which field broke and why.
- 01
Declared on the field
A template field — or your own schema’s field — carries its validators. The parameterised ones take your pattern, your allowed set, your bounds, your permitted countries.
- 02
Run on the resolved value
The cascade decides which source column feeds the field first. Validation then happens on the value about to be written, not on the raw cell in the file.
- 03
Real algorithm where one exists
Mod-97 for IBAN, the GS1 check digit for GTIN and GLN, Luhn for NPI. Where a code system publishes no check digit, the format is checked and named as a format check.
- 04
Flagged, or gated
A failure comes back as
{ col, code, message }. On a medium- or high-risk template the commit itself is gated on a human, not just annotated.
| Validator | What it checks | Example |
|---|---|---|
| regex | Custom pattern match | ^756\.\d{4}\.\d{4}\.\d{2}$ |
| enum | Value inside an allowed set | paid | unpaid | overdue |
| date_range | Date within bounds | 1900-01-01 … today |
| number_range | Number within bounds | 0 … 120 |
| Well-formed address | ada@example.org | |
| phone | Phone shape, optional country set | +41 44 000 00 00 |
| url | Well-formed URL + SSRF refusal | https://example.org |
| iban | Mod-97 checksum (+ per-country length in strict mode) | CH93 0076 2011 6238 5295 7 |
| bic | SWIFT / BIC structure | UBSWCHZH80A |
| uuid | RFC 4122 UUID | 9f1c8e2a-…-4c1d-… |
| gtin | GS1 check digit (8/12/13/14 digits) | 4006381333931 |
| pharmacode | Swiss pharmacode shape, 4–7 digits | 1234567 |
| fhir_reference | ResourceType/id reference form | Patient/123 |
| loinc_code | LOINC format | 4548-4 |
| icd10_code | ICD-10 format | E11.9 |
| atc_code | ATC format | A10BA02 |
| cpt_code | CPT format | 99213 |
| npi | NPI Luhn check | 1234567893 |
| gln | GLN check digit (13 digits) | 7601234567890 |
Try it
One row in, every failure out
Check a single row against a template before you build the rest of your import. Public, unauthenticated, nothing persisted — 200 requests an hour per IP.
curl https://api.adaptivmapr.com/v1/validate-row \
-H "Content-Type: application/json" \
-d '{
"template_id": "bank_accounts_v1",
"row": {
"account_holder": "Ada Lovelace",
"iban": "CH93 0076 2011 6238 5295 8",
"bic": "UBSWCHZH80A"
}
}'{
"template_id": "bank_accounts_v1",
"ok": false,
"errors": [
{ "col": "iban", "code": "iban_checksum", "message": "IBAN checksum failed" }
]
}POST /v1/validate-rowis public and unauthenticated — 200 requests an hour per IP, one row in,{ ok, errors[] }out. Nothing is stored.- In a real import the same rules run at COMMIT, on the resolved value about to be written, through
validateRowsDetailed()— so a bad row is flagged or skipped before it reaches your system of record. - A resolver-backed validator may substitute the canonical code it resolved, so the value that gets committed is the normalized one rather than the string the source happened to use.
The surface
Every route this page actually has.
One public route to check a single row while you build, and the commit-time surfaces where the same rules actually gate a write.
- POST
/v1/validate-rowOne row against one template, { ok, errors[] } out. Nothing persisted. 200 requests an hour per IP.no key - POST
/v1/uploads/{id}/validateDry-run the confirmed mapping over a whole stored upload before you commit any of it.read scope - PATCH
/v1/uploads/{id}/mappingsConfirm the mapping. Returnsrequires_hitl+hitl_statuson any medium- or high-risk template — 13 of the ones the engine ships.commit scope - POST
/v1/uploads/{id}/commitWhere the validators actually bite: rows that fail are flagged or skipped, and never written.commit scope - GET
/v1/templates/{id}Every field of a template with its label, its multilingual hints and the validators declared on it.no key
Limits & failure modes
What it refuses to do — and the code it says it with.
A validator that cannot fail is decoration. These are the refusals — the codes you branch on, and the two places where we deliberately do less than you might expect.
iban_checksumThe IBAN is well-shaped but its mod-97 remainder is not 1.
A single transposed digit is caught here and nowhere else. { type: "iban", strict: true } additionally enforces the per-country length.
ssrf_blockedA url field points at a private range, a link-local address or the metadata service.
A validator on a field that later gets fetched is a security boundary. require_https tightens it to TLS only.
428 hitl_requiredA commit on a medium- or high-risk template without hitl_approved: true, with MAPR_HITL_ENFORCE=1.
Production runs with enforcement on. Review the mappings, then re-commit with the flag — the refusal names the template and its risk level.
required_missingA field marked required resolved to an empty value.
Reported per column rather than as one opaque row failure, so a partially bad import tells you exactly which field to fix.
Format, not checksumLOINC, ICD-10, ATC, CPT and pharmacode.
None of these publishes a check digit we could verify, so we validate the format and say so. Pharmacode in particular is a 4–7 digit shape check, not a checksum.
No snomed_codeYou go looking for a SNOMED validator.
Removed in June 2026. SNOMED CT requires an Affiliate License and no template used it, so carrying it was a compliance liability with no benefit. It is not coming back.
What it costs
One prepaid wallet, drawn down per call.
No free tier. Top up from $10 — the balance is shared across the phi-cloud suite — and every operation draws it down at the rate below. An optional $6/mo plan grants a credit that resets each billing cycle instead; overage falls back to the wallet.
Every validator
Free
Pure compute, no network, no model — including the checksums. They add no token cost to any import they run inside.
POST /v1/validate-row
No key, no charge
Public and unauthenticated at 200 requests an hour per IP. Nothing is stored, so it is safe to wire into your own test suite.
A validated commit
$0.001
The flat per-map fee for the map itself. Validation is part of the map, not a line item on top of it.
A PHI/enterprise-routed run multiplies the whole charge by 1.2 (+20%), flat fee included — and only when the run genuinely got that routing. PHI stays locked until the workspace accepts the BAA in Settings → Security & Data. Full pricing
Questions
The ones asked before signing.
What it costs, what leaves your network, and the claims we will not make.
When do validators run?
Are these just regexes?
Why is there no SNOMED validator?
Can I add my own rules to a field?
What happens on a high-risk template?
Keep reading
The rest of the same engine.
One key, from a messy file to your schema.
Top up a $10 prepaid wallet and start mapping. In schema-only mode only your headers and three clamped rows ever leave you.