Customer Onboarding API
A proposed extension to the Codex FX API: programmatic customer onboarding, replacing form-based
intake. A customer approved here appears in GET /api/v1/customers and can transact immediately —
same auth, same conventions, same error shapes as the rest of the platform.
Design principles
◆Proofs, not documents
Requirements are proof categories (entity_formation, tax_registered, …). Any accepted
local document, registry enrichment, or structured field can satisfy one. Adding a country adds registry rows,
not endpoints.
⚡Registry enrichment first
Where a public registry exists — Receita Federal (BR), FINTRAC (CA MSBs), Companies House (GB) — the proof is satisfied live at create time and never becomes a document request.
✳AI classification at upload
Every document is classified and parsed synchronously: type, entity name, dates, freshness. A stale or mismatched document is rejected in the response with the reason — not discovered at review.
▸Progressive by default
GATE proofs decide first movement (a capped approval); DEFERRED proofs collect while capped rails run. RFIs are generated from open proofs and auto-close as they satisfy.
Authentication
Bearer JWT in the Authorization header, exactly as the Codex FX API.
The live prototype accepts any bearer token; requests without one return 401 UnauthorizedError.
Errors
Errors reuse the Codex FX shape — {"message": "…", "code": "…"} with a typed code.
| Status | Code | When |
|---|---|---|
400 | BadRequestError | Invalid body, missing required field, unreadable document |
401 | UnauthorizedError | Missing or invalid bearer token |
404 | NotFoundError | Unknown customer, person, or route |
Pagination
List endpoints return {"data": […], "hasMore": bool} and accept startingAfter,
endingBefore and limit — the Codex FX cursor convention.
Webhooks
Reuses the existing POST /api/v1/webhook-subscriber mechanism. New event types:
| Event | Fires when |
|---|---|
onboarding.customer.created | A customer enters DRAFT |
onboarding.document.classified | A document finishes AI classification (accepted or rejected) |
onboarding.customer.approved | Submit resolves to APPROVED_CAPPED — customer is transactable |
onboarding.rfi.opened | Submit finds open gates and generates an RFI |
onboarding.rfi.closed | The last open RFI item's proof is satisfied |
Quickstart: 5 calls to a decision
Onboard a Brazilian PSP end to end. Every response below is from the live prototype — nothing is mocked.
Create the customer
Send the legal entity and its CNPJ. The registry check runs inside this call: an ATIVA record from Receita Federal satisfies entity_formation, entity_active_status and tax_registered before you have uploaded anything.
curl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"legalEntityName": "Nu Pagamentos S.A.",
"countryCode": "BR",
"businessType": "COMPANY",
"registrationNumber": "18.236.120/0001-58",
"website": "https://nubank.com.br",
"expectedMonthlyVolumeUsd": 5000000,
"requestedRails": [
"USDC_PAYOUT",
"USD_WIRE"
],
"accountPurpose": "Supplier payouts to US vendors settled in USDC",
"idempotencyKey": "demo:nu:001"
}'{
"customerId": "cust_ob_he8wuf7n",
"status": "DRAFT",
"registryEnrichment": {
"source": "Receita Federal via BrasilAPI",
"status": "ATIVA",
"active": true
}
}Add the people
UBOs, directors, signers. Sanctions/PEP screening runs synchronously — a hit returns screeningStatus: "HIT" with EDD questions in the same response. Mapping ≥75%% of ownership satisfies beneficial_owners_identified.
curl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/persons \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"fullName": "David Velez Osorno",
"role": "UBO",
"ownershipPercent": 80
}'Upload corporate documents
Base64 JPG/PNG/PDF. Each document is classified against the country's accepted types, parsed, freshness-checked, and the proofs it satisfies are applied — all in the response. A Contrato Social closes governance_framework_documented and ownership_structure_documented in one upload.
curl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/documents \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"fileName": "contrato_social.pdf",
"contentBase64": "data:application/pdf;base64,JVBERi0…"
}'Verify the principal's identity
One call per UBO/signer. The extracted name must match the screened person; the document must show all four corners and be valid for 6+ months. When every principal verifies, principal_identity_verified closes.
curl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/persons/per_x1k9c2ab/identity-document \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"fileName": "passport.jpg",
"contentBase64": "data:image/jpeg;base64,/9j/4AAQ…"
}'Submit for a decision
Attestation by an authorized signer, then an immediate decision. All gates satisfied → APPROVED_CAPPED with a facility sized to expected volume, and the customer appears in GET /api/v1/customers. Open gates → an RFI generated from exactly those proofs.
curl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/submit \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"attestedBy": "David Velez Osorno"
}'{
"status": "RFI_REQUIRED",
"openGates": [
"governance_framework_documented",
"…"
],
"rfiId": "rfi_21q23sgd"
}Documents & classification
Classification is not OCR-and-hope. Each jurisdiction has a registry of accepted document types with issuer, freshness rule, and the proofs each type carries. The model classifies to the most specific candidate using the first-page title and issuing authority as the authoritative signal — or returns other with its reasoning.
| Outcome | What you get |
|---|---|
accepted: true | Proofs applied to the customer immediately; freshness.status: "FRESH" where a rule exists (e.g. Certidão Simplificada ≤ 90 days) |
accepted: false, stale | freshness.status: "STALE" — right type, too old; re-request with the rule in hand |
accepted: false, wrong type | detectedType: "other" plus reasoning naming what the document actually is |
Working with RFIs
Nobody writes an RFI. When /submit finds open gates, the RFI is that list — each item references the proof it exists to satisfy. Because items are proofs rather than document names, the customer can answer with whichever accepted document (or registry fact) closes the proof.
Items auto-close as proofs satisfy: upload the missing Contrato Social and both governance items resolve without an analyst touching the case. When the last GATE item closes, re-submit for the decision. Deferred-phase items keep collecting after approval while capped rails run.
Hosted onboarding links
Not every customer integrates an API. POST /api/v1/onboarding/links returns a magic link to the hosted flow at codex-ui.omniboarder.com — no portal account, valid 7 days. Both paths write the same canonical record, so a customer can start on the hosted form and finish over the API, or vice versa, and the compliance console sees one case either way.
Create an onboarding customer
/api/v1/onboarding/customersCreates a customer in DRAFT and computes its requirement set (proofs) from country, business type and expected volume. Where a live public registry is available, enrichment runs inline: for BR, the CNPJ is verified against Receita Federal and entity_formation + entity_active_status are satisfied with zero documents.
Body
legalEntityNamestringrequiredcountryCodestringrequiredbusinessTypeenumrequiredCOMPANY, FINANCIAL_INSTITUTION, SOLE_PROPRIETORregistrationNumberstring | nulloptionalwebsitestring | nulloptionalexpectedMonthlyVolumeUsdnumber | nulloptionalrequestedRailsarrayoptionalUSDC_PAYOUT, USDT_PAYOUT, USD_WIRE, EUR_SWIFTaccountPurposestring | nulloptionalidempotencyKeystringoptionalcurl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"legalEntityName": "Nu Pagamentos S.A.",
"countryCode": "BR",
"businessType": "COMPANY",
"registrationNumber": "18.236.120/0001-58",
"website": "https://nubank.com.br",
"expectedMonthlyVolumeUsd": 5000000,
"requestedRails": [
"USDC_PAYOUT",
"USD_WIRE"
],
"accountPurpose": "Supplier payouts to US vendors settled in USDC",
"idempotencyKey": "demo:nu:001"
}'{
"customerId": "cust_ob_he8wuf7n",
"legalEntityName": "NU PAGAMENTOS S.A. - INSTITUICAO DE PAGAMENTO",
"countryCode": "BR",
"businessType": "COMPANY",
"registrationNumber": "18.236.120/0001-58",
"status": "DRAFT",
"riskTier": null,
"facilityUsd": null,
"registryEnrichment": {
"source": "Receita Federal via BrasilAPI",
"legalName": "NU PAGAMENTOS S.A. - INSTITUICAO DE PAGAMENTO",
"status": "ATIVA",
"active": true
},
"requirements": [
{
"proof": "entity_formation",
"phase": "GATE",
"status": "SATISFIED",
"satisfiedBy": "registry_enrichment"
},
{
"proof": "entity_active_status",
"phase": "GATE",
"status": "SATISFIED",
"satisfiedBy": "registry_enrichment"
},
{
"proof": "tax_registered",
"phase": "GATE",
"status": "SATISFIED",
"satisfiedBy": "registry_enrichment"
},
{
"proof": "governance_framework_documented",
"phase": "GATE",
"status": "OPEN",
"satisfiedBy": null
},
{
"proof": "…",
"phase": "…",
"status": "…",
"satisfiedBy": null
}
],
"createdAt": "2026-08-10T11:07:41.000Z"
}List onboarding customers
/api/v1/onboarding/customersCursor pagination matching the Codex FX convention.
Query parameters
startingAfterstringoptionalendingBeforestringoptionallimitintegeroptionalcurl https://codex-api.omniboarder.com/api/v1/onboarding/customers \ -H "Authorization: Bearer <token>"
{
"data": [
{
"customerId": "cust_ob_he8wuf7n",
"legalEntityName": "NU PAGAMENTOS S.A. - INSTITUICAO DE PAGAMENTO",
"countryCode": "BR",
"status": "RFI_REQUIRED",
"createdAt": "2026-08-10T11:07:41.000Z"
}
],
"hasMore": false
}Get an onboarding customer
/api/v1/onboarding/customers/{customerId}Returns the customer with its live requirement set.
Path parameters
customerIdstringrequiredcurl https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n \ -H "Authorization: Bearer <token>"
{
"customerId": "cust_ob_he8wuf7n",
"status": "RFI_REQUIRED",
"riskTier": "LOW",
"requirements": [
{
"proof": "principal_identity_verified",
"phase": "GATE",
"status": "OPEN",
"satisfiedBy": null
}
],
"createdAt": "2026-08-10T11:07:41.000Z"
}Update an onboarding customer
/api/v1/onboarding/customers/{customerId}Updates mutable fields; requirements are recomputed.
operating_address_confirmed without a utility bill.Path parameters
customerIdstringrequiredBody
websitestringoptionalexpectedMonthlyVolumeUsdnumberoptionalaccountPurposestringoptionaloperatingAddressstringoptionalcurl -X PATCH https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"operatingAddress": "Rua Capote Valente 39, Sao Paulo, SP"
}'{
"customerId": "cust_ob_he8wuf7n",
"status": "DRAFT",
"requirements": [
{
"proof": "operating_address_confirmed",
"phase": "GATE",
"status": "SATISFIED",
"satisfiedBy": "attested"
}
]
}Get requirements
/api/v1/onboarding/customers/{customerId}/requirementsThe smart union of Codex's compliance floor and the customer's rails/volume/entity-type, expressed as proofs. GATE items block first movement; DEFERRED items collect while capped rails run.
Path parameters
customerIdstringrequiredcurl https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/requirements \ -H "Authorization: Bearer <token>"
{
"data": [
{
"proof": "entity_formation",
"phase": "GATE",
"status": "SATISFIED",
"satisfiedBy": "registry_enrichment"
},
{
"proof": "governance_framework_documented",
"phase": "GATE",
"status": "OPEN",
"satisfiedBy": null
},
{
"proof": "sof_verified",
"phase": "DEFERRED",
"status": "OPEN",
"satisfiedBy": null
}
],
"hasMore": false
}Add a person (UBO / director / signer)
/api/v1/onboarding/customers/{customerId}/personsSanctions/PEP screening runs synchronously on entry; a hit returns screeningStatus: HIT with EDD questions rather than silently queueing.
screeningStatus: "HIT" with the EDD questions to resolve it — in the flow, not three days later.Path parameters
customerIdstringrequiredBody
fullNamestringrequiredroleenumrequiredUBO, DIRECTOR, SIGNERownershipPercentnumber | nulloptionalcurl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/persons \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"fullName": "David Velez Osorno",
"role": "UBO",
"ownershipPercent": 80
}'{
"personId": "per_x1k9c2ab",
"fullName": "David Velez Osorno",
"role": "UBO",
"ownershipPercent": 80,
"screeningStatus": "CLEAR",
"eddQuestions": []
}List persons
/api/v1/onboarding/customers/{customerId}/personsPath parameters
customerIdstringrequiredcurl https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/persons \ -H "Authorization: Bearer <token>"
{
"data": [
{
"personId": "per_x1k9c2ab",
"fullName": "David Velez Osorno",
"role": "UBO",
"screeningStatus": "CLEAR"
}
],
"hasMore": false
}Upload a document (AI-classified synchronously)
/api/v1/onboarding/customers/{customerId}/documentsAccepts JPG/PNG/PDF as base64. The document is classified against the jurisdiction's accepted types, fields are extracted, per-document freshness rules are enforced, and the proofs it satisfies are applied — all in the response. A document that is stale, unreadable, or not an accepted type returns accepted: false with the reason.
Path parameters
customerIdstringrequiredBody
fileNamestringrequiredcontentBase64stringrequireddeclaredTypestring | nulloptionalcurl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/documents \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"fileName": "trade_licence.png",
"contentBase64": "data:image/png;base64,iVBORw0KG…"
}'{
"documentId": "doc_6i6mp3nb",
"fileName": "trade_licence.png",
"detectedType": "other",
"confidence": 0.97,
"reasoning": "This is a UAE Trade License issued by Fujairah Creative City - Media Free Zone, not a Brazilian corporate document. None of the candidate types apply.",
"entityName": "ATLANTIC GLOBAL FZE",
"issueDate": null,
"freshness": {
"ruleDays": null,
"status": "NOT_APPLICABLE"
},
"proofsSatisfied": [],
"accepted": false
}List documents
/api/v1/onboarding/customers/{customerId}/documentsPath parameters
customerIdstringrequiredcurl https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/documents \ -H "Authorization: Bearer <token>"
{
"data": [
{
"documentId": "doc_6i6mp3nb",
"detectedType": "other",
"accepted": false
}
],
"hasMore": false
}Upload a person's identity document
/api/v1/onboarding/customers/{customerId}/persons/{personId}/identity-documentPassport / national ID / licence. Extracts holder details, enforces the identity standards (all four corners visible, not expiring within 6 months) and name-matches against the screened person.
verified: false with each issue named.Path parameters
customerIdstringrequiredpersonIdstringrequiredBody
fileNamestringrequiredcontentBase64stringrequiredcurl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/persons/per_x1k9c2ab/identity-document \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"fileName": "passport.jpg",
"contentBase64": "data:image/jpeg;base64,/9j/4AAQ…"
}'{
"personId": "per_x1k9c2ab",
"verified": true,
"docType": "passport",
"holderName": "DAVID VELEZ OSORNO",
"expiryDate": "2031-04-22",
"confidence": 0.98,
"issues": []
}Submit for decision
/api/v1/onboarding/customers/{customerId}/submitRequires attestation. If every GATE proof is satisfied the customer is approved with a capped facility (APPROVED_CAPPED) sized to expected volume and appears immediately in GET /api/v1/customers; open gates instead open an RFI (RFI_REQUIRED) whose items auto-close as proofs satisfy.
APPROVED_CAPPED with a facility sized to expected volume, visible in GET /api/v1/customers at once. Open gates → an RFI generated from exactly those proofs.Path parameters
customerIdstringrequiredBody
attestedBystringrequiredcurl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/submit \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"attestedBy": "David Velez Osorno"
}'{
"customerId": "cust_ob_he8wuf7n",
"status": "RFI_REQUIRED",
"riskTier": "LOW",
"facilityUsd": null,
"openGates": [
"governance_framework_documented",
"ownership_structure_documented",
"directors_list_documented",
"principal_identity_verified"
],
"rfiId": "rfi_21q23sgd"
}List RFIs
/api/v1/onboarding/customers/{customerId}/rfisRFIs are generated from open proofs at submit; items reference the proof they exist to satisfy.
Path parameters
customerIdstringrequiredcurl https://codex-api.omniboarder.com/api/v1/onboarding/customers/cust_ob_he8wuf7n/rfis \ -H "Authorization: Bearer <token>"
{
"data": [
{
"rfiId": "rfi_21q23sgd",
"status": "OPEN",
"createdAt": "2026-08-10T11:14:02.000Z",
"items": [
{
"proof": "governance_framework_documented",
"status": "OPEN"
},
{
"proof": "ownership_structure_documented",
"status": "OPEN"
},
{
"proof": "directors_list_documented",
"status": "OPEN"
},
{
"proof": "principal_identity_verified",
"status": "OPEN"
}
]
}
],
"hasMore": false
}Create a hosted onboarding link
/api/v1/onboarding/linksReturns a magic link to the hosted onboarding flow (no portal account needed) for customers who won't integrate the API.
Body
legalEntityNamestring | nulloptionalcountryCodestring | nulloptionalcurl -X POST https://codex-api.omniboarder.com/api/v1/onboarding/links \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"legalEntityName": "TappaPay Ltd",
"countryCode": "CM"
}'{
"linkId": "oblnk_wxdtbl9d",
"url": "https://codex-ui.omniboarder.com/?link=oblnk_wxdtbl9d",
"expiresAt": "2026-08-17T11:20:54.553Z"
}Glossary
| Term | Meaning |
|---|---|
| Proof | The atomic unit of a requirement — a fact that must be established (entity_formation, tax_registered). Satisfied by a document, a registry enrichment, or a structured field. |
| Gate | A proof in the GATE phase. Open gates block a decision; all gates satisfied means an immediate capped approval. |
| Deferred proof | A proof collected after first movement, while capped rails run — e.g. sof_verified, financial_position. Never blocks the first approval. |
| Smart union | The requirement set for a customer: Codex's compliance floor ∪ whatever their rails, volume and entity type additionally demand. Computed at create, recomputed on every change. |
| Registry enrichment | A live public-registry lookup at create time (Receita Federal, FINTRAC, Companies House). Proofs it satisfies never become document requests. |
| Classification | The synchronous AI step on every upload: document type, entity name, dates, freshness — with reasoning, so a rejection explains itself. |
| Freshness rule | A per-document-type maximum age (Certidão Simplificada: 90 days). Stale documents are rejected at upload, not at review. |
| Screening | Sanctions/PEP check that runs when a person is added. HIT returns EDD questions inline. |
| Attestation | The authorized signer's declaration at submit — the legal anchor for the application. |
| APPROVED_CAPPED | The progressive-onboarding decision: transactable now, with a facility sized to expected volume, while deferred proofs collect. |
| RFI | Request for information — generated from open proofs at submit, items auto-close as proofs satisfy. |
| Facility | The prefunding credit limit granted on approval; visible to the prefunding desk immediately. |
| Canonical record | The single customer record shared by the API, hosted flow, compliance console, RFI manager and prefunding desk. Everything reads and writes the same spine. |
| UBO | Ultimate beneficial owner. Principals (UBOs and signers) must pass identity verification for principal_identity_verified to close. |
Sandbox
Fires real requests at codex-api.omniboarder.com — the same instance behind these docs. Any bearer
token works. Create a customer and the sandbox remembers its customerId (and personId)
for the follow-up calls, so you can walk the whole quickstart from this panel.
// pick an endpoint and hit Send