Test this flow in sandbox. Drive it end-to-end with simulated money and
deterministic controls — start with the sandbox
quickstart, then customer KYC
simulation for this flow, and the cheat
sheet for every magic value and simulate endpoint.
Prerequisites
- An API key for the environment you’re integrating against. See Authentication.
- A webhook endpoint subscribed to
application.approvedandapplication.rejected. See Webhooks. - The customer’s primary country (ISO 3166-1 alpha-2 or alpha-3, e.g.
USorUSA).
Flow
- Discover the onboarding requirements for the customer’s country.
- Upload each required document and keep the returned
doc_...ids. - Submit the application with the collected fields and document ids.
- Each person completes a Conduit-hosted identity check — Conduit emails them the link, or you deliver it yourself.
- Listen for
application.approvedorapplication.rejected. - Fetch the new customer.
Step 1 — Discover requirements
Requirements are country-specific. Call the discovery endpoint to learn which fields and documents to collect. Never hardcode them.fields[] (customer-level data to collect), documents[] (the customer-level document checklist), and individualRequirements[] (per-person requirements). A control person is a beneficial owner or controlling person of the business — the individuals you list in ownership.persons[]. Each individualRequirements[] row is one role and carries how many persons it needs (minCount), the scalar fields[] each must submit, and the documents[] each must supply. Read each row’s required flag, not its presence: a field can be listed and still be optional. A residential address, for example, is listed on every role, and turns required: true where enhanced due diligence applies. A proof-of-address document is listed only where it is owed. Upload each per-person document with POST /v2/documents and attach the returned id to that person’s ownership.persons[i].documentIds[] — never the top-level documentIds[].
minDocuments is the authoritative document floor: the number of customer-level documents you must upload before you can submit. When it is 1, attach at least one document from documents[] to the top-level documentIds[] — submitting with an empty documentIds is rejected with 422 ONBOARDING_NOT_READY. When it is 0, documents are optional at submit. The documents[] rows are the checklist of acceptable types, not per-row required flags.
The abridged example below includes per-person fields[] and documents[] rows for illustration — those particular rows read as they do under enhanced due diligence; treat whatever your own discovery response lists as the contract.
fields[].pointer is an RFC 6901 JSON pointer (e.g. /businessInfo/taxId). Render your collection UI from fields[], and validate against each field’s type, required, and constraints. enum fields carry the closed allowedValues set — the values above are abridged for the example; the live response returns the full set, and you must send a value verbatim from it (e.g. "C-Corporation", never an abbreviation like "C_CORP"). See requirements reference for the full schema.
Some fields are required only conditionally, and discovery advertises this through a conditions[] array on the field rather than a bare required: true — check both. companyClassification.primaryIndustryOther becomes required only once companyClassification.primaryIndustry is "other_industry", and businessActivity.otherActivityDescription becomes required only once businessActivity.regulatedOrRestrictedActivities includes "other_regulated_activity". A request that triggers the condition and omits the follow-up field is rejected with 422 ONBOARDING_NOT_READY.
Country codes are normalized server-side. You can send alpha-2 (
US) or
alpha-3 (USA); the response always echoes alpha-3.Discover requirements for a specific industry
Discovery also takes an optionalindustry query parameter. Adding it
returns the fields, documents, and person requirements tied to that
industry, alongside the country-based set.
key from GET /v2/onboarding/policy-subjects?axis=industry —
fetch that list to render an industry picker, and submit the selected key
back as companyClassification.primaryIndustry. Omit industry to get the
industry-agnostic response.
An industry value the catalog does not recognize is refused rather than
silently ignored, so a typo never passes through as “no industry selected.”
industry applies to a business subject only. Passing it alongside
subjectType=individual is rejected, since an individual carries no
industry classification.Step 2 — Upload documents
Upload each document the requirements ask for. The endpoint is multipart with an optionalpurpose form field. Ordinary onboarding documents can omit it; identity attestations use purpose=kyc. Repeat once per file.
id. Customer-level documents (e.g. incorporation papers) go in the top-level documentIds[]; documents that belong to a specific person (e.g. their proof of address) go in that person’s ownership.persons[].documentIds[] — one per document listed on that person’s role in individualRequirements[].documents[].
Step 3 — Submit the application
Turn eachfields[].pointer into a nested object (/businessInfo/taxId → businessInfo: { taxId }), attach the document ids, and POST to /v2/onboarding. Send every required field — a missing one is rejected with 422 ONBOARDING_NOT_READY listing it.
For US submissions, registeredAddress.state must be an ISO 3166-2 code (e.g. US-NY) — discovery advertises the field as an enum of the accepted codes, and submit rejects anything else. For other countries the field is free text; we still recommend an ISO 3166-2 subdivision code (e.g. MX-CMX for Mexico City) so the value is unambiguous.
Every other address discovery lists follows a looser rule on the same principle. When operatingAddress, or a person’s address where discovery lists one, declares country as the US, its state must name a US state, territory, or DC — US-NY, NY and New York are all accepted — and its postalCode must be a US ZIP (12345 or 12345-6789). Submit rejects a subdivision that belongs to another country, such as Baja California Sur, with 422 ONBOARDING_NOT_READY pointing at that address’s own state. Each address is judged against the country it declares, so a person who lives outside the US keeps a free-text state even when the business is a US entity.
Unlike registeredAddress.state, these two rules are not advertised in discovery. Discovery describes one jurisdiction per request, and these depend on the country each address carries in the payload, which the client has not sent yet. Submit is where they are enforced, and the rejection names the offending field.
A person may need their own address. Discovery lists a residential address on every person’s role, and the person owes it where the row reads required: true — which is jurisdiction- and diligence-dependent, e.g. under enhanced due diligence. A proof-of-address document is owed where discovery lists one on that role. Both are separate from the business address, and both come from that person’s role in individualRequirements[]: the residential address is among its fields[] (submit it under ownership.persons[i].address, same shape as registeredAddress), and the proof of address is among its documents[]. Upload the proof with POST /v2/documents and attach its id to that person’s ownership.persons[i].documentIds[] — never the top-level documentIds[], which is for business-entity documents only. Do not upload the person’s government ID here; the hosted identity check in Step 4 captures it.
202 Accepted with the new application in processing. The customer does not exist yet — the customerId field is omitted from the response until the application reaches approved.
Pass your own
clientReferenceId to correlate the application with a record
in your system. It is echoed back on the response and on every webhook for
this application. Allowed shape everywhere the field appears: 1-255 characters
from A-Za-z 0-9 _ - : . — no spaces.Step 4 — Each person verifies their identity
Once the application is inprocessing, every person you listed in ownership.persons[] completes a short Conduit-hosted identity check before the application can be approved. You don’t build this flow — Conduit hosts it and issues each person a link. What the check involves depends on the diligence level the application requires: a government-ID capture, plus a live selfie where enhanced due diligence applies — the hosted flow adapts on its own, so you never branch on it. This captures the person’s government ID (and the selfie, where required) — their proof of address is something you upload yourself in Step 3, not part of this check.
The link reaches each person through two independent channels — use either or both:
Conduit emails it directly (default). Each person is emailed at the email you submitted for them. A per-organization setting controls this and it is on by default; ask your Conduit account manager to turn it off if you’d rather be the only channel.
You deliver it yourself. Regardless of that email setting, you can receive each person’s link and deliver it through your own channel (your app, SMS, email). Two ways to get it:
- Pushed — an
idv_link.createdwebhook fires once per created verification session, as soon as that session is ready, whenever you’re subscribed. That is normally one event per person, and a person who is re-verified receives another one — you don’t have to turn the direct email off. The event is queued together with the verification itself, so it is never silently skipped — in the rare case the verification provider is briefly unreachable it arrives a little later. The pull endpoint below reaches the same provider, so during that window it can return502 KYC_UPSTREAM_UNAVAILABLE; retry it once the provider recovers. It carriespersonReferenceId, theurl, and a deprecatedshortUrl. See Webhooks. - Pulled — fetch a fresh link on demand for any person:
personReferenceId is Conduit’s stable id for each person — you don’t submit it. Conduit assigns it and echoes it under persons[] on the application, so you can drive the pull endpoint by polling alone, with no webhook:
The direct email and the
idv_link.created webhook are independent: the email
setting only controls whether Conduit emails each person directly, and the
webhook fires (as above) for verifications you’re subscribed to either way.
Turn the email off only if you want your own channel to be the sole one.Step 5 — Listen for the result
When review completes, Conduit delivers one of two webhooks. Both include yourclientReferenceId.
application.approved — the customer is now active. customerId is populated.
application.rejected — no customer is created. resubmittable says whether a corrected application will be considered. failureCode (machine-readable) accompanies every rejection. failureMessage (human-readable) is the specific correction to make on a resubmittable rejection and a fixed contact-support line with no per-field detail on a final one (see Handling a rejection); it is omitted when no reason was recorded. A resubmittable rejection also carries resubmissionGuidance, a fixed line telling you the documents already uploaded stay valid for the new submission. customerId is absent when the rejection occurs before customer creation, which is the common case for a customer_onboarding rejection.
Step 6 — Fetch the customer
On approval, retrieve the customer with thecustomerId from the webhook.
Handling a rejection
A rejection is a decision on the submission, not on the customer. The application record itself is terminal, but the customer can still be onboarded through a fresh submission unless the rejection was final. For acustomer_onboarding rejection, no customer is created and customerId is absent from the webhook. The application.rejected webhook carries resubmittable, plus failureCode (machine-readable) and failureMessage (human-readable) when a specific reason is available. All three are also readable from GET /v2/applications/{applicationId} after the fact, so if you miss the webhook you can fetch them from the resource.
resubmittable is the field to branch on:
Branch on
resubmittable, not on the code. Any failure code we add later reads as false until it is deliberately made resubmittable, so the boolean stays correct as the code list grows.
failureMessage follows the same split. On a resubmittable rejection it is the specific reason to surface to the applicant, so they can correct and resubmit. On a final one it is always the same fixed contact-support line, with no per-field or per-document detail — treat it as a cue to route the customer to support, not as text to parse.
You don’t have to rely on the webhook alone. Confirm an application’s status at any time — this reads the same rejection reason, so it also works if a webhook was missed:
status, resubmittable, failureCode, and failureMessage — the same information the webhook delivers — and the list endpoint (GET /v2/applications) carries them on each rejected row, so a polling integrator can read the outcome directly. customerId stays absent because a customer_onboarding rejection does not create a customer.
Correct and resubmit
A rejection does not block the business. A rejected application no longer counts as active. This path applies whenresubmittable is true. When it is false the decision is final and a new submission will reach the same outcome, so stop here and route the customer to support instead.
Once you’ve corrected the data, submit a fresh application for the same tax ID and beneficial owners. Submit it as a new POST /v2/onboarding with:
- a new
Idempotency-Key— reusing the rejected submission’s key replays its original response instead of creating a new application. - the same
clientReferenceId— keeps every attempt correlated to one record in your system. - the same
documentIds, in both the top-leveldocumentIds[]and eachownership.persons[i].documentIds[]— documents you already uploaded stay valid, so do not re-upload them. Upload a replacement only for a documentfailureMessageasks you to correct, and pass its new id in place of the old one, in the same array it came from.
To abandon an application that is still in review, before any decision, call
POST /v2/applications/{applicationId}/cancel. Approved and rejected
applications are terminal and cannot be cancelled.Onboarding an individual
Everything above onboards a business — the default subject. To onboard a natural person, declaresubjectType and send a person instead of a company. Individual onboarding is available to organizations enrolled in KYC reliance and enabled for individuals; otherwise discovery and submit return 422 SUBJECT_REQUIREMENTS_NOT_AVAILABLE. The lifecycle is the business one without the hosted identity check: you attach the account holder’s identity verification attestation, submit one application, then listen for application.approved or application.rejected.
Set subjectType on both the discovery call and the submit body. Omitting it onboards a business, so an existing business integration needs no change (see Onboarding an Individual). For an individual, the country query parameter — and the person’s own address.country — is their country of residence, the jurisdiction that decides what they owe.
Discover individual requirements
fields[] carries the account-holder-level descriptors — the activity object, the politically-exposed-person question, and the consent — while the single ACCOUNT_HOLDER row in individualRequirements[] carries the person’s own fields (legal name, date of birth, nationality, address) and their documents. It never lists business envelopes.
Submit an individual application
An individual application names exactly one person — the account holder, with the roleACCOUNT_HOLDER — and carries no business envelope; sending any is rejected (the individuals reference lists which ones). Alongside the person it sends an activity object (the account holder’s due-diligence descriptors), a regulatoryHistory flag, and the certification consent. Send each enum value verbatim from the discovery response’s closed sets. Attach the account holder’s identity verification attestation id to their documentIds[] — the person owes it under KYC reliance.
202 Accepted with the application in processing. There is no identity-check step for an individual — you already attached the account holder’s verification attestation — so go straight to listen for the decision. For everything a person owes, see Onboarding an Individual.