Load KYC profiles from a provider you did not choose.
Onboarding a KYC vendor, or migrating off one, means importing identity profiles with risk ratings attached. Map them to kyc_profiles_v1 — a high-risk template that comes back flagged for review every single time.
A JSON array of objects with camelCase keys — the parser reads it natively and the union of keys becomes the header row.
The job
What you are actually trying to get done
You are switching KYC providers, or absorbing a book of business, and the profiles come out in the outgoing vendor’s shape. The data is identity data with a risk rating attached — exactly the category that should not be loaded by a script nobody reviewed. The job is to map it once, validate the dates and identifiers, and have the review flag be a property of the data rather than a habit.
Where it lands
KYC profiles
kyc_profiles_v1 · risk: high
date_of_birth carries a date_range validator of 1900-01-01 → today, so an epoch-zero default or a year typed as 2206 fails rather than entering the file. risk_rating is an enum of low / medium / high — the vendor’s own five-band scale has to be mapped deliberately, not guessed.
A JSON array of objects is parsed natively — the union of keys, in first-seen order, becomes the header row the cascade works from.
POST /v1/uploads
2
Map the camelCase
normalize() strips punctuation and case, so legalName → legal_name and nationalIdentifier → national_id land on the free layers. dob is a shipped hint on date_of_birth.
POST /v1/uploads/:id/match
3
Map the risk scale explicitly
riskBand maps to risk_rating, but the vendor’s band vocabulary is not ours. The enum forces the decision into the open: values outside low / medium / high fail validation instead of arriving unlabelled.
PATCH /v1/uploads/:id/mappings
4
Validate identity fields
national_id against the template’s regex, date_of_birth against its range, verified_at as a date. Errors carry the row index and the field.
POST /v1/uploads/:id/validate
5
Review, then commit
High-risk, so the flag is on every response. Commit inline, to a signed webhook, or into a destination connector — the same commit can do more than one of those.
requires_hitl: true
In code
JSON in, validated profiles out
POST /v1/convert takes the JSON inline and returns the mapped rows in one call, with the routing it used reported on the response.
→ routing: "inline" · JSON is parsed in-process, nothing egressed to read it
routing: "inline" on the response means nothing left to a general-data host. Tabular input — json, csv, tsv, xml, sql, xlsx, parquet, and Word/PowerPoint tables — is always parsed in-process.
kyc_profiles_v1 is high-risk and the flag is derived from that field, not from a per-id list. Add a high-risk template to the catalogue and it is covered automatically.
Convert requires a schema: either a template ref, a saved custom schema, or an inline definition. POST /v1/schemas/from_text generates one from an English description if you do not have one yet.
Uploads (which may carry sensitive data) and connectors (which carry secrets) stay on the service-role plane with explicit tenant_id scoping. The user-JWT dashboard plane is gated by Supabase RLS.
Where the work lands
Which layer resolves this file
Layer
What it does here
Auto-accepts at
Cost
1 · statistics
Header→field pairs confirmed on an earlier batch from the same provider
≥100 @ 95% · ≥20 @ 100%
Free · deterministic
2 · heuristic
“dob”, “legal name”, “national id”, “risk rating” — shipped hints in five languages
A provider-specific label with no shared vocabulary
≥ 0.78
Cheap, cached · off without an embedding key
5 · ai
One batched call over the leftovers, constrained to the unclaimed field set
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
kyc_profiles_v1 is a high-risk template, so PATCH /v1/uploads/:id/mappings returns requires_hitl: true and hitl_status: "pending_review" on every call, for every workspace, with nothing to configure.
Honest limit · The flag is derived from the template’s risk field and is advisory in v1 — we set it, your workflow owns the queue. The upload is not server-side blocked. A native AgentGate approval queue is roadmap, not v1.
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.
Standard routing, region still pinned
This job carries no protected health information, so it runs on standard routing with no surcharge. The workspace region pin still applies — the routing class picks the model catalogue; the region decides where compute may run.
Mechanism · resolveRunPhi() in lib/agreements.ts resolves the axis per run from phi_mode in the body, falling back to tenants.phi_mode (default false). PHI is available on this job too if your data class calls for it — it costs +20% and needs the BAA.
Questions
The things people actually ask
No — kyc_profiles_v1 applies a conservative regex (4–20 characters, alphanumerics and hyphens) that catches empty values, free text and obviously malformed entries. National identity schemes differ per country and several are not publicly checksummable. Per-country checking belongs in your own schema, where you can express the rule you are actually obliged to apply.
Map them deliberately. risk_rating is an enum of low, medium and high, so a five-band value fails validation rather than arriving as an unlabelled string. If your obligations require five bands, define your own schema with POST /v1/schemas that declares them — packs are a catalogue, not a gate, and the cascade maps to any schema you give it.
Usually not — a KYC profile is PII under GDPR and its equivalents, not protected health information under HIPAA. So this job runs on standard routing with no surcharge by default. If your data class does call for PHI routing, it is available at +20% and is locked until your workspace accepts the BAA in-app.
Uploads live in Cloudflare KV with a 24-hour TTL and are mirrored to Supabase under an explicit tenant_id. Deleting the workspace runs one implementation — audit event first, Supabase cascade, KV purge, cache clears — and neither erasure route reports success unless the tenant row is actually gone.