Skip to main content
Onboard a business customer along the golden path: discover what a country requires, upload the documents, submit the application, and react to the result. Conduit is submit-and-listen. Submit one application. Receive a webhook when it is approved or rejected. There is nothing to poll. For the end-to-end state and recovery map, see The Onboarding Lifecycle. This guide onboards a business — the default subject. To onboard a natural person, see Onboarding an individual: it shares every step below, differing only in the subject you declare and the fields you send.
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.approved and application.rejected. See Webhooks.
  • The customer’s primary country (ISO 3166-1 alpha-2 or alpha-3, e.g. US or USA).

Flow

  1. Discover the onboarding requirements for the customer’s country.
  2. Upload each required document and keep the returned doc_... ids.
  3. Submit the application with the collected fields and document ids.
  4. Each person completes a Conduit-hosted identity check — Conduit emails them the link, or you deliver it yourself.
  5. Listen for application.approved or application.rejected.
  6. 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.
The response has three parts: 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 document floor: the number of documents the submission must carry. Every document counts, at the top level and on each person. When it is 1, a submission with no document at all is rejected with 422 ONBOARDING_NOT_READY. When it is 0, documents are optional at submit. Submit checks the count, not the type. Review checks each documents[] row and its minCount. An application with a missing row stays in processing until the document arrives. Attach customer-level documents to the top-level documentIds[]. The documents[] rows are the checklist of acceptable types, not per-row required flags. A document with conditions is owed only when they hold on your submission. 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.
Each 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. For an individual customer, taxIdType depends on the person’s nationality and residence. A US national sends SSN. A non-US national who lives in the US sends SSN or ITIN. Everyone else sends NATIONAL_ID, PASSPORT, DRIVER_LICENSE or RESIDENT_CARD, with taxIdCountry set to the country that issued it. Add the optional nationality query parameter to the requirements call to get the list for a given nationality. Without it, discovery lists SSN and ITIN for a US resident because it does not know the nationality, and submit refuses ITIN from a US national with 422 ONBOARDING_NOT_READY at /ownership/persons/0/taxIdType. A malformed SSN or ITIN gets the same 422 at /ownership/persons/0/taxIdNumber, in the same 422 ONBOARDING_NOT_READY list as the other field problems. Submit also refuses a NATIONAL_ID issued by CAN, NLD, IRL, AUS or JPN, and a NATIONAL_ID that does not match the format of its issuing country. See Identifiers that are not accepted for each country. 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. An optional field can carry conditions[] too: the conditions[] of companyClassification.llcTaxClassification match a US LLC only. Send Partnership, C-Corp or S-Corp to state how the IRS taxes the LLC. A multi-member LLC that made no election is a Partnership. Banks that need this answer open the account faster when you send it.
Country codes are normalized server-side. You can send alpha-2 (US) or alpha-3 (USA); customer reads return alpha-2 (US), except activity.countriesOfActivity, which keeps the alpha-3 codes you sent.

Fields that depend on an earlier answer

Conduit does not fix the order of your questions. A field or document that depends on an answer carries that answer’s test in its conditions[]. Show these follow-up questions after the user gives the answer, and use each field’s required flag to know which ones submit enforces. Until the answer is given, submit does not check the follow-up fields.

Discover requirements for a specific industry

Discovery also takes an optional industry query parameter. Adding it returns the fields, documents, and person requirements tied to that industry, alongside the country-based set.
The value must be a 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 optional purpose form field. Ordinary onboarding documents can omit it; identity attestations and identity documents use purpose=kyc. Repeat once per file.
Keep each returned 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 each fields[].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. We clean every address you send — registeredAddress, operatingAddress, and each person’s address — against the country that address declares, before we store it:
  • Characters are converted to plain ASCII: México becomes Mexico, São Paulo becomes Sao Paulo.
  • state that names a subdivision of the address country is stored as its ISO 3166-2 code. We recognize the code (MX-CMX), the code without its country prefix (CMX), and the subdivision’s name (Ciudad de México), so the customer reads back MX-CMX. Discovery shows a valid code for the requested country as the field’s example.
Any other value is kept as you sent it. For US submissions, registeredAddress.state must name a US subdivision — discovery advertises the field as an enum of ISO 3166-2 codes (e.g. US-NY). Submit also accepts NY and New York, stores them as US-NY, and rejects anything else. 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.
The endpoint returns 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.
Idempotency-Key is required. Reuse the same key when retrying a submission so a network failure can’t create a duplicate application. A submission that matches an existing active application is rejected with 409 ONBOARDING_ALREADY_SUBMITTED. A business matches on its tax ID. Only a business that sends no tax ID matches on the tax ID of a person in its ownership, so one person can own several businesses that send a tax ID, all in review at the same time. An individual matches on the account holder’s identity.

Step 4 — Each person verifies their identity

Once the application is in processing, 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.created webhook 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 return 502 KYC_UPSTREAM_UNAVAILABLE; retry it once the provider recovers. It carries personReferenceId, the url, and a deprecated shortUrl. See Webhooks.
  • Pulled — fetch a fresh link on demand for any person:
Once the application is approved, rejected or cancelled, the person no longer needs to verify, and this endpoint returns 409 APPLICATION_INVALID_STATUS. 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:
Send url. It is resumable, not single-use: the person can refresh it, reopen it, and finish on a second device, and it stays valid as long as their verification session does. That makes it bearer material — deliver it straight to the person and never log it.shortUrl is deprecated and will be removed. Despite the name it is not a shortening of url — it is a separate one-time link that opens once and stops working about five minutes after the person opens it, so anyone who steps away mid-check comes back to a dead page. It is short enough for SMS, which is exactly why it was used there; prefer url even when that means shortening it yourself, and use a shortener you control, since the link is bearer material.Verification sessions are created asynchronously, so the pull endpoint returns 404 until a person’s session exists (react to idv_link.created, or retry) and 409 once that person’s verification has already settled.
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 your clientReferenceId. 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, the specific reason on a final rejected_by_ops rejection, and a fixed contact-support line on a final compliance_denied 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 the customerId 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 a customer_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 rejection with compliance_denied 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. On a final rejection with rejected_by_ops it is the specific reason, for you to show or log. When Conduit needs more information, a resubmittable rejection also carries reviewQuestions: a list of questions, on both the webhook and the resource. Show them to the applicant, and address each one in the corrected data and documents of the fresh submission. The field is absent when there are no questions and on a final rejection. 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:
The response reports 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 when resubmittable 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-level documentIds[] and each ownership.persons[i].documentIds[] — documents you already uploaded stay valid, so do not re-upload them. Upload a replacement only for a document failureMessage asks 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. A repeat cancel of a cancelled application returns 200 with the application unchanged. Approved and rejected applications are terminal and return 409. An application in review whose automated review could not finish also returns 409: contact support to close it.

Onboarding an individual

Everything above onboards a business — the default subject. To onboard a natural person, declare subjectType and send a person instead of a company. Individual onboarding must be enabled for your organization. Contact your Conduit account manager. Until it is, 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 and original ID document, 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

The Individual quickstart shows a full individual example: this discovery response, the submit body and the approved customer object. The response has the same shape, resolved for a person, and uses the same split described above: top-level 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 role ACCOUNT_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 and ID document ids to their documentIds[]. Send the date the verification was completed, as the report shows it, in identityVerificationDate. It is optional, but send it. Without it, approval depends on reading the date from the report, and the application can wait for a manual review.
The response is the same 202 Accepted with the application in processing. A second submission for the same account holder’s tax ID returns 409 ONBOARDING_ALREADY_SUBMITTED with details.applicationId while this application is in review, and 409 CUSTOMER_ALREADY_ONBOARDED with details.customerId after approval. The duplicate check ignores dashes, spaces and letter case: 524456701 and 524-45-6701 are the same tax ID. Send an SSN or ITIN as nine digits. Dashes and spaces are removed before the number is checked. The phone number follows the phone rule. There is no identity-check step for an individual — you already attached the account holder’s verification attestation and ID — so go straight to listen for the decision. For everything a person owes, see Onboarding an Individual. Once approved, Get a customer by ID returns type: "individual" and the account holder’s name fields, residential address, activity answers and regulatoryHistory.isPoliticallyExposedPerson, in the same nested shape you submitted. The address country is ISO 3166-1 alpha-2; activity.countriesOfActivity keeps the alpha-3 codes you sent, so you can send them back in an update. To change an activity answer or the politically-exposed-person answer later, send only the leaves that change under activity or regulatoryHistory in a customer update; after approval the customer holds the new value. An update that makes the person politically exposed, or moves the residence to a country that calls for enhanced due diligence, also owes the source of wealth and the evidence that onboarding asks for.

What happens next

The customer is active but has no features yet. Add a Virtual Account so they can receive funds — see Add Virtual Accounts.