Skip to main content

Verification (KYB)

A business is verified before it can move money: you send its company and people data, our pre-check applies its exclusion rules, and our banking partner's compliance team reviews and approves it. Until then the account's verification status is pending, and in production that blocks payouts and conversions.


Where it sits​

Account statusMeansPayouts and FX execute
pendingSigned up; not yet approvedProduction: refused, 403 account_not_verified, before anything is sent to the bank. Sandbox: allowed — send Cxc-Simulate: account_not_verified to see the refusal there.
activeApprovedAllowed
suspendedSuspended or rejected by CashXChain. A business our partner rejects stays pending.Refused everywhere, 403 account_not_verified with next_action: contact_support

The refusal reads "account is not KYB/KYC-verified (active); funds movement is blocked until verification completes". Of our own routes, only POST /v1/banking/payouts and POST /v1/banking/fx/execute check it.

In production our banking partner adds its own gate, and it is wider. Until it approves the business, it refuses to act for it at all: opening or listing accounts, registering beneficiaries, quoting and paying all fail — with 503 service_unavailable, or, before any referral exists, 400 bad_request "customer has no corporate reference at the banking partner — onboard via KYB referral first". So in production nothing on the money API works for a business before approval. Build and test the flow in the sandbox.

On approval our partner opens EUR, GBP, USD and CAD accounts for the business. GET /v1/banking/accounts then lists the ones in currencies you hold — at first, the local currency for the country the business is registered in; see Accounts.

Reading the status. GET /v1/accounts/me returns kyb_status and can_move_money, but only to a dashboard session or a master key. A secret key cannot read its own account's verification status; with one, the signal is the referral's status.


The calls, in order​

Every KYB route is on the money API, so it takes a secret key, a master key or a dashboard session. None takes an Idempotency-Key.

Plain text
1  POST /v1/banking/kyb/precheck/preview                optional — will this be blocked?
2 POST /v1/banking/kyb/referrals pre-check, then send to the partner
3 POST /v1/banking/kyb/referrals/{external_id}/persons more directors, owners, contacts
POST /v1/banking/kyb/referrals/{external_id}/parent-entities only for corporate owners
4 POST /v1/banking/kyb/referrals/{external_id}/documents/slots company documents
POST /v1/banking/kyb/referrals/{external_id}/persons/{person_external_id}/documents/slots
POST /v1/banking/kyb/referrals/{external_id}/parent-entities/{parent_entity_external_id}/documents/slots
5 POST /v1/banking/kyb/referrals/{external_id}/finalise
6 GET /v1/banking/kyb/referrals/{external_id} poll until approved

1. Preview — optional, records nothing​

Answers "will this application be blocked?" while the form is still open. It screens the jurisdiction and industry only, writes no record and calls no one.

Bash
curl -s -X POST "$CXC_BASE/v1/banking/kyb/precheck/preview" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-d '{"legal_name": "Example Casino Ltd", "country_code": "IR", "industry_code": "92.00"}'
JSON
{
"blocking_findings": [
{"rule": "jurisdiction_blocklist", "severity": "block", "detail": "country IR is on the blocked-jurisdictions list"},
{"rule": "industry_blocklist", "severity": "block", "detail": "industry code 92.00 matches blocked prefix 92"}
]
}

An empty list does not mean the application will pass — it means nothing blocks it outright. The full pre-check runs inside the referral.

POST /v1/banking/kyb/precheck runs the full pre-check without sending anything to the partner, and records the result: the jurisdiction and industry rules, and a sanctions-list screen where one is configured. It does not look the business up in a company register — existence comes back verified: false — which is why nearly every pre-check ends in review. Its history is at GET /v1/banking/kyb/precheck-records.

2. Referral​

POST /v1/banking/kyb/referrals takes the whole application. The people and the company facts are needed up front — the application is checked for completeness before anything is sent, and an incomplete one is refused with 400 bad_request naming every gap:

FieldRequiredNotes
legal_nameYesAt most 50 characters on the production rail
country_codeYesISO 3166-1 alpha-2
contact_emailYes
registration_numberYesAt most 50 characters on the production rail
incorporation_dateYesYYYY-MM-DD
entity_categoryYesThe legal form, e.g. GmbH, Ltd
registered_addressYes{line1, line2?, city, postal_code?, region?, country_code}. The production rail also requires city and postal_code.
personsYesAt least one person who controls the business — role director, ubo, sole_trader or partner — and one primary_contact. A sole_trader covers both.
industry_codeProduction, unless business_profile.nature_of_business is sentThe nature of business. The pre-check screens industry_code.
business_profileProductionThe partner's business questionnaire — see below
complianceProductionhas_existing_bank_account, has_dispute, has_bankruptcy, is_regulated — each true or false, with the matching …_details text when true
trading_address, website, business_description, expected_monthly_volume, operating_countriesbusiness_description here feeds the pre-check only; the partner reads the one in business_profile

"Production" means the production rail, which the sandbox also uses when it refers to our partner's test environment (see below). On it, business_profile must answer: business_description (60 to 500 characters), payment_currencies, intend_payment_countries, receive_payment_countries, source_of_funds, years_of_trading, annual_revenue, expected_transaction_value, expected_transaction_volume, outgoing_value_per_year, anticipate_receiving_from, purpose_of_receiving, anticipate_sending_to and purpose_of_sending. Several take values from the partner's own lists; a value it does not accept is refused with 400 bad_request and the partner's reason. None of these answers is ever filled in for you.

A person has first_name, last_name, and roles — one or more of director, ubo, primary_contact, legal_representative, sole_trader, partner — plus date_of_birth, nationality, email, phone, address and the PEP flags. A ubo needs ownership_percentage. id_type is one of passport, identity_card, drivers_license, residence_permit.

The answer depends on the pre-check:

StatusoutcomeMeans
201referredSent to the partner. referral.external_id is the application id for every later call.
202manual_reviewThe pre-check raised something a person at CashXChain must clear before the application goes to the partner. Nothing was sent yet.
200already_referredThis account already has an application at the partner. The existing external_id is returned and nothing is created — safe to retry.
422kyb_precheck_blockedBlocked by the pre-check; the message names the findings.
400bad_requestThe application is incomplete or invalid; the message lists every gap. Nothing was sent.
JSON
{
"outcome": "referred",
"referral": {
"external_id": "corp_1830042517",
"status": "submitted",
"raw": {}
},
"precheck": {
"precheck_record_id": "0b6f…",
"decision": "pass",
"findings": [],
"existence": [],
"sanctions_hits": [],
"missing_onboarding_fields": []
}
}

In the sandbox the 201 already reads status: "approved". referral.raw is the partner's own onboarding payload, passed through; its shape is the partner's and may change.

3–5. Complete the application​

  • More people: POST …/{external_id}/persons with one person; 201 returns person_external_id.

  • Corporate owners: if the business is owned by another company, POST …/{external_id}/parent-entities with that company (role, entity_category, legal_name, registration_id, date_of_incorporation, email, address) and at least one of its own owners. 201 returns parent_entity_external_id and the owners' ids. Most applications never call it.

  • Documents are uploaded straight to the partner's storage, never through CashXChain. Ask for a slot, then upload into it:

    RouteBodyResponse
    …/documents/slotsdocument_type (Certificate of Incorporation, Articles of Association, Due Diligence Bundle, Others with other_document_type), content_type (application/pdf, pdf, image, doc, docx){"urls": [{"url": "…", "fields": {…}}]}
    …/persons/{person_external_id}/documents/slotsThe identity document — id_type, id_number, issued_country, id_expiry_date if it has one, id_content_type — and the proof of address: address_type (Utility Bill, Regulated Bank Statement, Government Issued Id, Government Issued Document, Other with document_name), address_content_typepresignedResponse: an object with one upload target per slot. Its keys depend on id_type — a passport has one ID side, a driving licence two.
    …/parent-entities/{parent_entity_external_id}/documents/slotsdocument_type (Certificate of Incorporation, Articles of Association, List of Directors, Structure Chart), content_type (application/pdf, pdf, image)As for company documents

    Each upload target is a url plus form fields: post the fields as multipart/form-data with the file last, within about ten minutes. Files under 5,120 bytes are rejected by the storage policy. If an upload fails or the slot expires, ask for a new slot — do not start a new application.

  • Finalise: POST …/{external_id}/finalise submits the application for review. 200 {"external_id", "status": "submitted"}; after it the application can no longer be amended. The partner refuses it until every document is in: the company's documents, an identity document and a proof of address for every person, and the documents of every parent entity. The partner does not say in advance which parent-entity documents it requires — send all four.

These calls answer 404 for an external_id that is not yours.

6. Wait for the decision​

HTTP
GET /v1/banking/kyb/referrals/{external_id}

This asks the partner, every time. In production the partner reports one thing: whether the business is approved. status is approved, or in_review for everything else — still in review, waiting on documents, or rejected. A rejection is not reported through the API — the application goes on reading in_review. The other values in the vocabulary (precheck_passed, submitted, info_requested, rejected) are not returned by this poll today. Poll every few minutes; decisions take days.

When the partner approves, your account becomes active and its bank accounts appear in GET /v1/banking/accounts.

No webhook for this

The kyb.updated event is not sent for a business — not when our partner approves, not on rejection or suspension, in either environment. Poll the referral.


Sandbox and production​

SandboxProduction
Where the application goesOur partner's test environmentOur partner
Pre-check result review — nearly every application, since there is no register lookupSent to the partner anywaySent to the partner, or held for a person at CashXChain (202 manual_review), depending on how review is configured. Handle both answers.
ApprovalImmediate, on our side: the 201 already reads status: "approved", and the account becomes activeWhen the partner approves, after its review — days
The status pollAsks the partner's test environment, which reviews by hand and can go on answering in_review after the sandbox has approved you. Trust the 201.The partner's answer: approved or in_review
Payouts while pendingAllowedRefused

The sandbox does not wait for anyone, and it approves more than referrals: an account that is pending or suspended is made active the next time it reads GET /v1/accounts/me or GET /v1/accounts/me/balance. In the sandbox, verification is never what blocks you. Treat sandbox approval as a fast-forward, not as evidence that a production application would pass — only the partner's review in production decides that.