REST API
api.adaptivmapr.com/v1, one HMAC bearer key.Start · The five-call quickstart
Docs
Map any CSV, Excel, SQL or JSON onto your target schema through the six-layer cascade — without shipping raw records to an LLM. Start with the quickstart, then jump to the reference section you need.
Quickstart
One multipart call starts an import: the parser reads your columns, the cascade proposes a mapping, you confirm it, and commit returns validated rows.
The full walkthrough — minting a key with the right scopes, schema-only versus full-data, and the requires_hitl flag — is five annotated calls on the quickstart page.
There is no client library, and none of these samples needs one: the API is plain HTTPS with a bearer token and JSON bodies, so the runtime you already have is the SDK.
Full quickstart# 1 · parse the file. No template is named here.
curl -X POST https://api.adaptivmapr.com/v1/uploads \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY" \
-F "file=@customers.csv"
# 2 · the target template is chosen on THIS call
curl -X POST https://api.adaptivmapr.com/v1/uploads/$UPLOAD_ID/match \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "target_schema": { "schema_id": "users_v1" },
"mode": "schema-only" }'api.adaptivmapr.com/v1, one HMAC bearer key.Start · The five-call quickstart
Start · Open the Workbench
Start · Install block
API reference
REST under https://api.adaptivmapr.com/v1. Authenticate with Authorization: Bearer mp_live_…. The pill on a row is the scope that key must carry — a key without it gets 403 insufficient_scope, naming the scope it needed. A dashboard pill means the route takes the session cookie only and refuses a bearer key outright.
/v1/uploadsMultipart file → detected columns, row count, ≤3 clamped sample rows and a 24h expires_at. CSV, TSV, XLSX/XLSM, JSON, XML, Parquet, and the tables inside .docx / .pptx.transform/v1/uploads/:id/matchRun the six-layer cascade. mode is schema-only or full-data.read/v1/uploads/:id/match/streamThe same run as Server-Sent Events, so a UI can show each layer as it resolves.read/v1/uploads/:id/mappingsYour overrides. Feeds the statistics layer, and returns requires_hitl for a medium- or high-risk template.commit/v1/uploads/:id/validateRun the per-row validators without committing — errors and warnings.read/v1/uploads/:id/commitValidated rows inline (≤10k), 500-row HMAC-signed webhook batches, or a direct write into a destination connector. output selects rows, FHIR, or a serialized file.commit/v1/uploads/:id/deliveryQueued-webhook delivery status — the state of the batches, not the rows./v1/transformUpload + match + validate in one call. An optional output attaches a serialized file alongside the grid.transform/v1/convertAny-to-any import — spreadsheet, PDF, image, audio, HTML, a URL, a SQL query. Returns your data.transform/v1/reshapeStructural transform: transpose, un-pivot, join, fan out to several sheets. output: "xlsm" carries a template workbook’s macros through unchanged.commit/v1/gatewayAny input in, your table populated. Returns a delivery report, not the data.commit/v1/matchStateless header→field matching. Schema-only by construction; the metered layer 5 is unreachable without a key. Optional table_name (your source table or sheet) gives the ranker its context on keyed calls.no key/v1/validate-rowStateless single-row validation against a template. Pure compute.no key/v1/layouts/lookupThe whole confirmed mapping for an exact header row, if you have mapped it before.read/v1/layouts/recordRecord a confirmed layout by hand. Commit already does this for you.commit/v1/layouts/driftstable / first_seen / drift plus an added-removed-common diff. Deterministic, no LLM.read/v1/templates33 pre-built templates with their fields, hints, validators and risk level.public/v1/templates/:idOne template, in full.public/v1/packsThe 7 domain packs and their template counts.public/v1/packs/:idOne pack and every template in it.public/v1/schemasYour own target schemas — list them, or register a new one./v1/schemas/:idRead or delete one of your schemas./v1/schemas/from_textDraft a schema from a prose description, or from a file’s own headers.transform/v1/schemas/generateGenerate a schema with the model. Draws the flat per-map fee./v1/keysList keys, or mint one — the secret is shown once. GET is the dashboard session only. POST takes a bearer key too, but only one carrying the parent-only keys:mint scope, and the child can never hold more than its parent.dashboard/v1/keys/:idRevoke a key. Enforced on every subsequent request by the server-side check.dashboard/v1/keys/:id/rotateMint a replacement and retire the old one after a grace_period_seconds overlap (0–3600). Session only — a bearerAuthorization header is refused outright with 400 bearer_not_supported rather than silently ignored.dashboard/v1/connectorsCreate and list ingest sources and write destinations; secrets are KEK-encrypted at rest. Connector CRUD (and /:id, /:id/test) is dashboard-only.dashboard/v1/connectors/:id/syncPull now, instead of waiting for the schedule. Session or bearer.connectors/v1/connectors/:id/ingestThe public inbound URL of a webhook_receiver connector — HMAC-verified, no bearer key.hmac/v1/connectors/:id/schemaThe columns of a destination table, introspected through the connector. ?table= overrides the saved one. Session or bearer.connectors/v1/connectors/:id/rotate-secretReplace a connector’s stored credential without re-creating it — new_secret, or new_auth for a multi-field login. Read the age back from /secret-meta. Session or bearer.connectors/v1/webhooks/testSend a signed test delivery to your endpoint.commit/v1/auditThe append-only audit trail for your workspace. Cursor-paginated — limit, after, since, until, kind.read/v1/usageWallet draw-down and per-operation usage. ?phi=1 adds your phi-cloud token consumption and cost.read/v1/pricingThe live rate card — the same numbers billing uses.read/v1/learningsCross-workspace learning status, plus /opt-in and /opt-out. Default is off.admin| Field | Type | What it does | |
|---|---|---|---|
file | file (multipart) | required | The file itself. Missing or not a file → 400 file_missing; over the size cap → 413 file_too_large with max_bytes in the body. |
header_row | integer | default 0 | Which row holds the headers. Raise it when a sheet opens with title or metadata rows. |
sheet_name | string | optional | Which worksheet to parse. Defaults to the first. |
multipart/form-data. There is no template field on this call — the upload only parses.
| Field | Type | What it does | |
|---|---|---|---|
target_schema.schema_id | string | required | A plain template id — patient_demographics_v1, users_v1. There is no sch_ prefix. Unknown ids return 404 template_unknown; a missing id returns 400 schema_id_required. |
mode | "schema-only" | "full-data" | default schema-only | full-data needs an active PHI entitlement on the workspace; without one the call returns 402 phi_gateway_required. |
use_ai | boolean | default true | Set false to stop at layer 4. Every column that would have reached the metered call comes back unmapped instead of billed. |
phi_mode | boolean | workspace default | Per-run routing override. Falls back to tenants.phi_mode, which defaults to false. An explicit PHI ask on a workspace with no accepted BAA returns 403 agreement_required. |
application/json. The response is { upload_id, template_id, mode, matches[], unmapped[], auto_accept_threshold, cascade_layers } — one proposal per detected column, each carrying the source layer that produced it.
| Field | Type | What it does | |
|---|---|---|---|
skip_invalid_rows | boolean | default false | Drop rows that fail a validator and report them in skipped. Without it, any row error fails the whole commit with 422 validation_failed. |
output | "rows" | "fhir" | file format | default rows | csv, tsv, xml, sql, json, xlsx, parquet. Anything else → 400 invalid_output. xlsm needs a source workbook and is refused here. |
webhook | { url, secret } | optional | Stream the result in batches of 500 to your endpoint, signed. Required above 10 000 accepted rows — inline delivery over that returns 413 inline_too_large. |
database | { connector_id, table?, on_conflict?, dry_run? } | optional | Write into a saved destination connector. Credentials are never taken from this body — only the connector id and the delivery options. |
hitl_approved | boolean | optional | The explicit human-review acknowledgement. Required to commit a medium/high-risk template when MAPR_HITL_ENFORCE is on; otherwise requires_hitl stays advisory. |
application/json, and every field is optional — an empty body commits the confirmed mapping as inline rows.
Two hostnames, one Worker. api.adaptivmapr.com/v1/… and adaptivmapr.com/api/v1/… both reach these handlers. A bare adaptivmapr.com/v1/… does not — it 404s, because the path rewrite is keyed on the API hostname.
The dashboard plane is separate. Routes under /api/v1/me/* authenticate with the Supabase session cookie rather than a bearer key, and are what the Workbench calls. They are not part of the public v1 contract and are not versioned with it.
Keys & scopes
API keys are HMAC-signed and self-contained: the workspace (tenant_id), the name and the granted scopes are carried in the token and verified by signature, so authentication needs no per-request key lookup. Pass the key as a bearer token in the Authorization header on every call.
read, transform, validate — deliberately not commit, connectors or admin. If your integration confirms mappings or commits rows, ask for commit at mint.| Scope | What it unlocks |
|---|---|
read | Read-only matching, validation, audit log, and usage queries. |
transform | Parse files, run the cascade, and call one-shot transform endpoints. |
validate | Run per-row validators (regex / iban / loinc / icd10 / etc). |
commit | Finalise mappings and deliver rows downstream (webhook + commit). Required for any write-back. |
connectors | Create, edit, sync, and delete connector records (S3, HTTPS, webhook receivers). |
admin | Workspace settings, cross-workspace learning toggles, session extension, workspace deletion. Treat as keys-mint-equivalent. |
Read off lib/scopes.ts. A key carries its scopes inside the signed token, so widening a key means minting a new one.
Rotating the signing secret is a two-step swap that never breaks live keys: move the current MAPR_KEY_SECRET to MAPR_KEY_SECRET_PREVIOUS, set the new value as MAPR_KEY_SECRET, and re-mint at your own pace. Verification does a constant-time compare against both secrets, so keys signed by the old secret keep working through the overlap window. Drop MAPR_KEY_SECRET_PREVIOUS once everything is re-minted. Mint, rotate and revoke in the dashboard.
Browser calls need a CSRF header. Bearer-authenticated requests are exempt — the key is not browser-replayable. But a mutating call carrying the dashboard session cookie to /api/v1/me/*, /api/v1/schemas*, /api/v1/keys, /api/v1/connectors, /api/v1/learnings/, /api/v1/webhooks/, /api/billing/ or /api/admin/ must echo the non-httpOnly am_csrf cookie back in an X-CSRF-Token header. Miss it and the deny is opaque — a flat 403, not a hint.
// Dashboard (session-cookie) calls to /api/v1/me/* and
// /api/v1/schemas* are CSRF-gated. Echo the am_csrf cookie back.
await fetch('/api/v1/me/emit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({ format: 'csv', headers, rows }),
}){ "error": "CSRF token missing or invalid",
"code": "csrf_token_mismatch" }Modes, PHI & retention
How much of your data leaves is one decision. Where the AI call runs is a different one. They are set independently.
Schema-only is the data-minimization mode: only headers plus ≤3 sample rows (≤80 characters each) ever leave your environment. The clamp is enforced at the HTTP edge on every route that accepts sample rows — clampForSchemaOnly() is the single chokepoint, so the API and the MCP tools cannot drift apart on it.
Full-data is gated on an active PHI entitlement. The cascade still runs in-process exactly as it does for schema-only; only the layer-5 LLM call leaves, and it goes to phi-cloud with X-PHI: true and an X-Region header so the gateway forces a PHI-eligible, in-region model. The call fails closed when the target host is not on the BAA-covered allow-list.
PHI routing is a third, orthogonal axis. phi_mode defaults to off — an unconfigured workspace runs standard, and every fail-soft path defaults to standard rather than silently upgrading unknown traffic. PHI routing costs +20% on the whole map charge and is locked until the workspace accepts the BAA/NDA in Settings → Security & Data; an explicit PHI ask without an acceptance returns 403 agreement_required with a pointer, never a silent downgrade. Unstructured input (documents, prose, audio, URLs) always routes as PHI, so asking for standard there returns 400 phi_required_for_full_data.
Retention. Uploads live in Cloudflare KV under a TTL your workspace sets — 24 hours by default, or 0, 168 or 720. The upload response carries the actual expires_at, which is the value to trust rather than any number on this page; 0 means the upload is destroyed on commit. Deleting the workspace destroys the tenant row and its cascade, purges KV and clears the caches in one operation. Data flow, sub-processors and controls are documented at /legal/security.
Usage & cost
Every header runs through six layers, cheapest-first, and the moment a layer accepts a column the layers below it never run. Only the LLM layer bills third-party AI tokens, so keeping columns out of it is the single biggest cost lever in the system.
source: "ranker" in the response.source: "ai".The highest-leverage thing you can do to lower your LLM share is add multilingual hints to your templates: a hint that catches a header’s vocabulary pulls it up into the free heuristic layer, so it never reaches the metered call again.
What a map costs. There is no free tier. Every operation draws down a prepaid token wallet ($10 minimum, shared across the phi-cloud suite). A small flat per-map fee — a few tokens — is charged when a map materialises (commit, transform, convert, reshape, gateway), including a fully deterministic map and a layout-cache hit that used no AI at all. AI cleanup, convert and reshape additionally bill the phi-cloud tokens they consume; a PHI-routed run multiplies the whole charge by 1.2. Read consumption back with GET /v1/usage, and the live rate card with GET /v1/pricing.
Limits & pagination
Rate limits, size caps and page sizes, read off the route modules. Where a limit bites, the response says exactly how long to wait.
| Route | Requests | Window | Keyed by |
|---|---|---|---|
POST /v1/transform | 60 | 1 min | workspace |
POST /v1/convert | 60 | 1 min | workspace |
POST /v1/gateway | 60 | 1 min | workspace |
POST /v1/reshape | 30 | 1 min | workspace |
POST /v1/uploads/:id/match | 1 000 | 1 min | workspace |
POST /v1/match | 100 | 1 hour | client IP |
POST /v1/validate-row | 200 | 1 hour | client IP |
The limiter is two-tier: a shared Postgres counter is authoritative, with a per-isolate in-process counter as a fast pre-deny and as the fallback when the shared store is unreachable. Unlisted routes are limited by the wallet and by quota rather than by a request ceiling.
A 429 carries everything a client needs to back off exactly rather than guess: Retry-After in whole seconds, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Honour Retry-After when it is present; an exponential backoff that ignores it just spends your budget faster.
# Retry-After is authoritative — do not invent a backoff.
curl -sS -D headers.txt -X POST https://api.adaptivmapr.com/v1/transform \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY" \
-F "file=@roster.csv" -F 'target_schema_id=users_v1' -o body.json
grep -i '^retry-after\|^x-ratelimit' headers.txt
# retry-after: 37
# x-ratelimit-limit: 60
# x-ratelimit-remaining: 0
# x-ratelimit-reset: 37| What | Cap | What happens at the edge |
|---|---|---|
| Upload size | 10 MiB | 413 file_too_large, with max_bytes in the body. Tunable per deployment via MAPR_MAX_UPLOAD_BYTES. |
| Inline commit rows | 10 000 | 413 inline_too_large. Supply a webhook to go past it — there is no higher inline tier. |
| Webhook batch | 500 rows | Fixed. A 12 000-row commit is 24 signed deliveries, each with its own batch_index. |
| Schema-only sample | 3 rows · 80 chars | The data-minimization clamp, enforced at the HTTP edge on every route that accepts sample rows. |
| Upload retention | 24h by default | Cloudflare KV TTL, set per workspace to 0, 24, 168 or 720 hours. After expires_at the upload_id 404s; re-POST the file. |
| Audit page | 50 default · 200 max | GET /v1/audit?limit=…&after=…. next_cursor is null on the last page. |
Only one route returns an unbounded collection, and it is cursor-paginated rather than offset-paginated: GET /v1/audit walks the append-only trail newest-first. Pass limit (50 by default, 200 maximum), then follow next_cursor into after until it comes back null. A cursor is an opaque row id — do not construct one. Narrow the window with since / until (ISO-8601) and the set with kind, a comma-separated subset of upload.create, mapping.confirm, mapping.commit, billing.event, key.create, key.revoke and connector.sync. An unrecognised kind is dropped rather than rejected.
The catalogue routes (/v1/templates, /v1/packs) return the whole set in one response and are edge-cached; there is no page parameter because there is nothing to page.
# Page backwards through the audit trail. 50 rows by
# default, 200 max; next_cursor is null on the last page.
curl "https://api.adaptivmapr.com/v1/audit?limit=200&kind=mapping.commit" \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY"
# then follow the cursor
curl "https://api.adaptivmapr.com/v1/audit?limit=200&after=$NEXT_CURSOR" \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY"{
"workspace_id": "…",
"rows": [ { "kind": "mapping.commit", "chain_seq": 4127,
"prev_hash": "…", "hash": "…", "payload": { /* metadata */ } } ],
"next_cursor": "aud_01J…",
"total_in_window": 4127
}Errors
Errors come back as { error: { code, message, … } } with a stable code, and the codes worth branching on carry the field you need to act.
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | No bearer key, a bad signature, or an expired key. |
| 401 | key_revoked | Signature is valid but the key is on the deny-list. Verification fails closed — an unreachable revocation store is treated as revoked. |
| 403 | insufficient_scope | Valid key, wrong scopes. The body names required_scope and present_scopes. |
| 403 | agreement_required | A PHI-routed run on a workspace that has not accepted the BAA. The body carries settings_url. |
| 403 | csrf_token_mismatch | A session-cookie call to a CSRF-gated prefix with no X-CSRF-Token. Bearer calls are exempt. NOTE: this one is refused by middleware before the route runs, so the code is at body.code, not body.error.code. |
| 400 | unsupported_format | A media or prose-only file on a tabular route. Use /v1/convert — except .xlsb, which must be re-saved. |
| 400 | invalid_output | output is not one of json|csv|tsv|xml|sql|fhir|xlsx|xlsm|parquet. |
| 400 | xlsm_requires_template | xlsm asked for on a rows→file surface (/v1/me/emit, /v1/convert). Macros need a source workbook; use /v1/reshape with a template, or xlsx. |
| 400 | phi_required_for_full_data | Standard routing asked for on unstructured input. Documents, prose, audio and URLs always extract as PHI. |
| 402 | insufficient_credit | Prepaid wallet is empty or below the cost of the run. The body carries topup_url and minimum_usd. |
| 402 | quota_exceeded | The workspace is over its monthly upload or metered-row allowance for its tier. Distinct from an empty wallet: topping up does not clear it. |
| 428 | hitl_required | A commit of a medium- or high-risk template without hitl_approved, on a deployment with MAPR_HITL_ENFORCE on. Only then — the flag is advisory by default. |
| 422 | needs_clarification | The understanding pass found a mismatch and asked a question before spending on codegen. Answer it, or re-run with skip_clarification. |
| 429 | rate_limited | Retry-After, X-RateLimit-Limit, -Remaining and -Reset are all on the response. |
A 429 carries Retry-After plus the X-RateLimit-* trio, so a client can back off exactly rather than guess.
The envelope is stable. Match on error.code, never on error.message — the message is written for a human and is allowed to improve. Three codes carry an extra field that turns the error into an instruction: insufficient_scope names required_scope and present_scopes, agreement_required carries a settings_url, and insufficient_credit carries topup_url and minimum_usd.
{
"error": {
"code": "insufficient_scope",
"message": "key lacks required scope",
"required_scope": "commit",
"present_scopes": ["read", "transform", "validate"]
}
}Retry 429 and 5xx. Everything else is a decision you have to change. A 401 or 403 is a credential or an agreement; a 400 or 422 is the request you sent; a 402 is an empty wallet, which does not refill because you tried again. Retrying those burns the flat per-map fee on each attempt for no new information.
// One handler for every failure mode the API has.
// RETRY: 429 (honour Retry-After) and 5xx. Nothing else.
// NEVER RETRY: 401/403 (credential or agreement), 400/422 (your request),
// 402 (an empty wallet does not refill by trying again).
const RETRYABLE = (s) => s === 429 || s >= 500
export async function call(path, init, attempt = 0) {
const res = await fetch(`https://api.adaptivmapr.com/v1${path}`, init)
if (res.ok) return res.json()
const body = await res.json().catch(() => ({}))
const code = body?.error?.code
if (RETRYABLE(res.status) && attempt < 4) {
const hinted = Number(res.headers.get('retry-after'))
const waitMs = Number.isFinite(hinted) && hinted > 0
? hinted * 1000 // the server told you; believe it
: 2 ** attempt * 500 // 5xx only: exponential, jittered below
await new Promise((r) => setTimeout(r, waitMs + Math.random() * 250))
return call(path, init, attempt + 1)
}
// These three are actionable, not transient — surface them to a human.
if (code === 'insufficient_scope') throw new Error(
`mint a key with "${body.error.required_scope}"`)
if (code === 'agreement_required') throw new Error(
`accept the BAA at ${body.error.settings_url}`)
if (code === 'insufficient_credit') throw new Error(
`top up at ${body.error.topup_url}`)
throw new Error(`${res.status} ${code ?? 'unknown'}`)
}MCP
AdaptivMapr ships an MCP server so an AI coding environment can call the cascade directly. Add the block below to your MCP config — ~/.cursor/mcp.json for Cursor, or the Claude Desktop config file — and restart the client. The ADAPTIVMAPR_API_KEY is optional: five tools run with no key at all.
{
"mcpServers": {
"adaptivmapr": {
"command": "npx",
"args": ["-y", "@adaptivmapr/mcp-server"],
"env": {
"ADAPTIVMAPR_API_KEY": "<optional — omit for the no-key tools>"
}
}
}
}No key · schema-only
Only headers and ≤3 short sample rows leave your machine; csv_preview never touches the network at all. With no key configured these run unauthenticated against a per-IP rate limit and the metered layers are unreachable — so they draw nothing from a wallet. The one exception once a key is set: match_headers sends it, adds the ranker and layer 5, and bills each map to that key’s workspace wallet (a 402 means the wallet is empty).
Key · full-data
These need a valid key and a funded wallet, and they bill. The commit tools return a delivery receipt, never the rows; commit_to_database writes through a destination connector saved in the dashboard. Layer-5 traffic routes to phi-cloud with X-PHI / X-Region when the run is PHI-routed — see Modes, PHI & retention.
Workbench
The Workbench is the hosted, chat-first surface over the same engine: attach files, write what you want, press Enter. The run shows as a transcript — the model’s reading of your request, the steps as they happen, then results as file cards you can open or download. It runs on the /api/v1/me/* plane with your dashboard session, not with an API key.
tenant_id, the same posture as uploads, and they are destroyed by the workspace erasure path. Remembered chats are audited as workbench.chat — ids, counts, bytes and routing, never content. Incognito chats leave no trail at all.Change the memory default in Settings → Security & Data. Set it there per workspace; flip a single chat from the composer.
Reshape
Column mapping cannot transpose a matrix, un-pivot a wide sheet, join two tables or fan one file out into thirteen worksheets. Reshape is the tier-2 path that can.
POST /v1/reshape (bearer, commit scope) and POST /v1/me/reshape (dashboard session) take your sources plus a target — a schema, or an example workbook whose sheets become the shape to fill. Every fresh run opens with one understanding pass that restates the task from the inputs; that reading anchors the generated program and is returned on both routes, so you can see what the run thought it was doing before it did it. A verdict of mismatch with a question stops the run before any codegen and returns 422 needs_clarification — answer it, or re-run with skip_clarification.
Progress streams. POST /v1/me/reshape/stream emits Server-Sent phase frames and then the same terminal JSON the one-shot returns, including workbook_sheets — which sheets the run produced and which it preserved from your template.
Macros travel. output: "xlsm" is the round trip: template sheets, rewritten data, and the source workbook’s VBA project intact. The macro-enabled format is the consent — every other output value strips macros, and stripping is all four of dropping the part, dropping the relationship, demoting the content type and warning the caller. On the rows→file surfaces there is no source workbook to take macros from, so xlsm is refused there with xlsm_requires_template rather than shipping a file whose name disagrees with its contents.
Reuse. With Save transforms for reuse on (the default), a reshape’s transform is stored so the next import with the same layout replays it — no AI, just the flat per-map fee.
Learning
Every correction you confirm feeds the statistics layer, so the same header resolves without AI next time. What is recorded is header-to-field statistics only — the normalized column name and the target field it was confirmed against. No cell values, no sample rows and no record content are stored in the learning signal.
tenant_id and enforced by row-level security — no cross-tenant sharing of statistics or layouts.POST /v1/learnings/opt-in adds anonymous header→field confirmations to a global rollup. Row values are never part of it, and the default is off.Layouts & drift
Beyond per-pair statistics, the whole confirmed mapping for an exact header row is cached as a layout, keyed by a sha256 fingerprint of the normalized header row. Commit records it automatically, and when the same header row arrives again POST /v1/layouts/lookup returns it so your importer can offer one-click “reuse last mapping”. A re-map that hits this cache uses no AI — you pay only the flat per-map fee. Layouts are workspace-scoped; there is no cross-tenant sharing.
# Offer one-click "reuse last mapping" for a known header row
curl -X POST https://api.adaptivmapr.com/v1/layouts/lookup \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "template_id": "users_v1",
"headers": ["Vorname", "Nachname", "E-Mail"] }'Drift detection reuses the same fingerprint. POST /v1/layouts/drift compares an incoming field-set for a known (workspace, template) against the newest confirmed layout and returns status: drift (or stable / first_seen) with the added, removed and common headers nested under diff — not at the top level. added carries your header verbatim; removed and common come back normalized, because the baseline is stored normalized and matching on the raw spelling is exactly what the fingerprint exists to avoid. It is fully deterministic — no LLM call — and source-agnostic.
# Detect schema drift for a known (workspace, template) — no LLM
curl -X POST https://api.adaptivmapr.com/v1/layouts/drift \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "template_id": "users_v1",
"headers": ["Vorname", "Nachname", "Phone"] }'{ "template_id": "users_v1",
"status": "drift",
"header_fingerprint": "7c1e…",
"baseline_fingerprint": "b91c…",
"baseline_last_used_at": "2026-09-19T11:02:14.000Z",
"baseline_use_count": 37,
"reordered_only": false,
"diff": { "added": ["Phone"],
"removed": ["e mail"],
"common": ["vorname", "nachname"] } }Connectors & webhooks
Connectors are configured per workspace under /v1/connectors, with their secrets encrypted at rest. They run in both directions: a source is read from (a file drop, an HTTPS or object-store location, a SQL-over-HTTP query, an inbound webhook), a destination is written into (a SQL endpoint, Supabase, a warehouse, an object store, a SaaS object). A source runs on demand — you trigger /sync — or on a schedule.
Outbound webhooks deliver validated rows to your endpoint on commit, in batches of 500. Each delivery is signed and carries the metadata a consumer needs to deduplicate and route it without parsing the body first.
POST /your-endpoint HTTP/1.1
content-type: application/json
x-mapr-signature: t=1756800000, v1=9f2c1e… # HMAC-SHA256 of "${t}.${body}"
x-mapr-template: patient_demographics_v1
x-mapr-fingerprint: b91c… # the mapping this batch used
idempotency-key: upl_8f3a92c1-batch-0 # stable per (upload, batch)
x-mapr-output: csv # only when output ≠ rows
x-mapr-fhir-resource: Patient # only when output = fhir
{ "batch_index": 0, "rows": [ /* ≤500 rows */ ] }The signature header is X-Mapr-Signature, formatted t=<unix_seconds>, v1=<hex_sha256> — the same envelope Stripe uses, including the space after the comma. The signed string is ${t}.${body}, computed with the webhook secret you set, over the raw request body exactly as received. Binding the timestamp into the signature is what gives you replay protection: reject anything whose signature does not recompute, and anything whose t is outside your tolerance — 300 seconds is what our own inbound verifier uses. Failed deliveries are retried and dead-lettered.
// Verify an AdaptivMapr delivery (Node 18+)
import { createHmac, timingSafeEqual } from 'node:crypto'
// header: "t=1756800000, v1=9f2c…" ← note the space after the comma
function parseSig(header) {
const out = {}
for (const part of header.split(',')) {
const [k, v] = part.trim().split('=')
out[k] = v
}
return out
}
export function verify(rawBody, header, secret, toleranceSec = 300) {
const { t, v1 } = parseSig(header)
if (!t || !v1) return false
// Replay window. The timestamp is unix SECONDS and is bound to the
// signature, so a captured body cannot be replayed with a fresh clock.
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false
// Sign the RAW body exactly as received. Re-serialising the parsed JSON
// reorders keys and changes whitespace, and the HMAC will never match.
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex')
// timingSafeEqual THROWS on a length mismatch — check first.
const a = Buffer.from(v1, 'utf8')
const b = Buffer.from(expected, 'utf8')
return a.length === b.length && timingSafeEqual(a, b)
}Schemas
A schema (template) is the shape you map onto. 33 ship across 7 packs — Core, CRM, E-commerce, Healthcare, Finance & Payments (PCI), HR & People (PII), and Evidence. Each field carries multilingual hints (DE / FR / IT / EN / ES) and typed validators; there are 19 validator types in all, from regex and iban through loinc_code, icd10_code, cpt_code, npi and gln. Healthcare templates additionally carry a fhir_resource mapping.
A template id is a plain string — patient_demographics_v1, users_v1 — and it is what you pass as target_schema.schema_id. There is no opaque sch_ handle to look up first.
Every template declares a risk level (low / medium / high). The 13 non-low templates surface requires_hitl: true and hitl_status: "pending_review" on the mappings response so you can gate your own review before commit. The flag is derived from risk, so a new risky template is covered the day it ships. It is advisory by default — the commit goes through — but it is not inert: with MAPR_HITL_ENFORCE on, a commit of a non-low template without hitl_approved: true is refused with 428 hitl_required.
List them with GET /v1/templates, read one with GET /v1/templates/:id, or browse the catalogue. Register your own under /v1/schemas, or draft one from prose with POST /v1/schemas/from_text. For the FHIR emit path see FHIR mapping, and for the field-level checks see the validator reference.
Troubleshooting
Every entry here is a real failure mode of the deployed system, with the reason it behaves that way. Most of them are a deliberate trade rather than a defect.
Why
A key minted without an explicit scope list gets read, transform, validate — deliberately not commit.
Fix
Read required_scope off the error body and mint a new key with it. Scopes live inside the signed token, so a key cannot be widened in place.
Why
The path rewrite is keyed on the API hostname. Only api.adaptivmapr.com/v1/… and adaptivmapr.com/api/v1/… reach the handlers.
Fix
Use the API host, or prefix with /api on the app host.
Why
A mutating session-cookie call to a CSRF-gated prefix without the X-CSRF-Token header. The deny is deliberately flat.
Fix
Echo the non-httpOnly am_csrf cookie back in the header. Bearer-authenticated calls are exempt — this only bites the dashboard plane.
Why
Three usual causes: reading X-AdaptivMapr-Signature (that is the inbound connector header — deliveries carry X-Mapr-Signature), splitting the value without trimming the space after the comma, or HMAC-ing a re-serialized JSON body.
Fix
Sign the raw request body as received, as ${t}.${body}, and compare in constant time. The worked verifier is in Connectors & webhooks.
Why
Layers 4 and 5 are config-gated and fail soft to OFF. With no embedding key and no LLM key the deterministic layers 1–3 are all that run — which is correct behaviour, not an outage.
Fix
Add multilingual hints to the target fields so the vocabulary resolves in the free heuristic layer, which is where you want it anyway.
Why
v1 auth layers a server-side revocation check on top of the HMAC verify, and it fails closed — an unreachable revocation store is treated as revoked.
Fix
Check status before re-minting. A database outage can never resurrect a dead key, which is the trade this fails-closed direction buys.
Why
Uploads live in KV until the TTL your workspace sets — 24 hours unless you changed it.
Fix
Read expires_at off the upload response and re-POST the file past it. Nothing is recoverable after the TTL — that is the retention promise, not a bug.
Why
Macro bytes under an .xlsx name — the format/extension conflict Excel refuses.
Fix
Ask for output: "xlsm" on /v1/reshape with a macro-bearing template; every other output value strips the VBA project cleanly instead.
Why
It is a BIFF12 binary workbook, not an OOXML zip.
Fix
Re-save it as .xlsx or .xlsm and re-upload.
Why
Webhook batches are delivered independently. The commit succeeded; one or more deliveries did not.
Fix
Read failed_batches on the response and poll GET /v1/uploads/:id/delivery. A non-empty failed_batches means a partial delivery — a 200 is not proof of a full one.
Still stuck? GET /api/health reports every component the Worker depends on — the /v1 API, persistence behind the Workbench, the MCP server, webhook delivery and macro editing — and the status page renders the same data.
Compliance
Every commit and confirmation is hashed and written to an append-only audit trail with the mapping fingerprint and accepted/skipped counts, so you can reconstruct exactly which rows landed from which file and when. Each row carries a chain_seq, its own hash and the prev_hash it chains to, which is what makes a gap or an edit detectable rather than merely unlikely. Read it back with GET /v1/audit, or export it from the dashboard, which also renders a print-for-auditor view over the same trail.
Schema-only mode minimises exposure because raw records never leave your environment. Full-data PHI processing is covered by a BAA you accept in-app and by in-region routing; AdaptivMapr is HIPAA-ready — HIPAA is not a certification anyone can hold — and SOC 2 is in progress. Data flow, sub-processors and security controls are documented at /legal/security.
Ready when you are
Top up a prepaid token wallet ($10 minimum, shared across the phi-cloud suite) and map your first file. Every map is a small flat fee; AI only bills when a column reaches it.
Prepaid token wallet · schema-only data-minimization mode · no free tier