> ## Documentation Index
> Fetch the complete documentation index at: https://docs.conduit.financial/llms.txt
> Use this file to discover all available pages before exploring further.

# Onboard a Customer

> Discover onboarding requirements, then submit a customer application

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](/guides/onboarding-lifecycle).

This guide onboards a **business** — the default subject. To onboard a natural person, see [Onboarding an individual](#onboarding-an-individual): it shares every step below, differing only in the subject you declare and the fields you send.

<Note>
  **Test this flow in sandbox.** Drive it end-to-end with simulated money and
  deterministic controls — start with the [sandbox
  quickstart](/sandbox/quickstart), then [customer KYC
  simulation](/sandbox/customer-kyc) for this flow, and the [cheat
  sheet](/sandbox/cheat-sheet) for every magic value and simulate endpoint.
</Note>

## Prerequisites

* An API key for the environment you're integrating against. See [Authentication](/authentication).
* A webhook endpoint subscribed to `application.approved` and `application.rejected`. See [Webhooks](/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.

```bash theme={null}
curl 'https://api.conduit.financial/v2/onboarding/requirements?country=US' \
  -H "x-api-key: YOUR_API_KEY"
```

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.

```json theme={null}
{
  "schemaVersion": "3",
  "context": "onboarding",
  "country": "USA",
  "fields": [
    {
      "pointer": "/businessInfo/taxId",
      "label": "Tax ID (EIN)",
      "type": "string",
      "required": true,
      "constraints": {
        "pattern": "^(?:\\d{2}\\-\\d{7})$",
        "example": "12-3456789"
      }
    },
    {
      "pointer": "/companyClassification/legalStructure",
      "label": "Legal structure",
      "type": "enum",
      "required": true,
      "allowedValues": [
        "C-Corporation",
        "S-Corporation",
        "Limited Liability Company (multi-member)",
        "Limited Partnership",
        "Sole Proprietorship"
      ]
    },
    {
      "pointer": "/companyClassification/primaryIndustry",
      "label": "Primary industry",
      "type": "enum",
      "required": true,
      "allowedValues": [
        "manufacturing",
        "non_luxury_clothing_retail",
        "health_care_providers",
        "real_estate",
        "utilities_electricity_gas_water"
      ],
      "options": [
        { "value": "manufacturing", "label": "Manufacturing" },
        {
          "value": "non_luxury_clothing_retail",
          "label": "Non-luxury Clothing Retail"
        },
        { "value": "health_care_providers", "label": "Health Care Providers" },
        { "value": "real_estate", "label": "Real Estate" },
        {
          "value": "utilities_electricity_gas_water",
          "label": "Utilities (Electricity, Gas, Water)"
        }
      ]
    }
  ],
  "documents": [
    {
      "canonicalType": "business_registration",
      "title": "Articles / Certificate of Incorporation"
    }
  ],
  "minDocuments": 1,
  "individualRequirements": [
    {
      "role": "any",
      "minCount": 1,
      "fields": [
        { "pointer": "/firstName", "type": "string", "required": true },
        { "pointer": "/email", "type": "email", "required": true },
        {
          "pointer": "/taxIdType",
          "type": "enum",
          "required": true,
          "allowedValues": ["SSN", "ITIN", "NATIONAL_ID"]
        },
        {
          "pointer": "/address/addressLine1",
          "type": "string",
          "required": true
        }
      ],
      "documents": [
        { "canonicalType": "PROOF_OF_ADDRESS", "title": "Proof of address" }
      ]
    }
  ]
}
```

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](/api-reference/customer-onboarding/discover-onboarding-requirements-for-a-country) 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](/kyb/individuals#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`.

<Note>
  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.
</Note>

### 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.

```bash theme={null}
curl 'https://api.conduit.financial/v2/onboarding/requirements?country=US&industry=fintech_companies' \
  -H "x-api-key: YOUR_API_KEY"
```

The value must be a `key` from [`GET
/v2/onboarding/policy-subjects?axis=industry`](/api-reference/customer-onboarding/discover-selectable-policy-subject-catalog-options-for-an-axis) —
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."

<Note>
  `industry` applies to a business subject only. Passing it alongside
  `subjectType=individual` is rejected, since an individual carries no industry
  classification.
</Note>

## 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.

```bash theme={null}
curl https://api.conduit.financial/v2/documents \
  -X POST \
  -H "x-api-key: YOUR_API_KEY" \
  -F "file=@articles-of-incorporation.pdf"
```

```json theme={null}
{ "id": "doc_034A0gCCW6w2ubcipmRoY8" }
```

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.

```bash theme={null}
curl https://api.conduit.financial/v2/onboarding \
  -X POST \
  -H "x-api-key: YOUR_API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: 8f3a1c2e-1b4d-4e9a-9c7f-2a6b5d8e1f00" \
  -d '{
    "clientReferenceId": "your-ref-001",
    "businessInfo": { "legalName": "Acme Inc", "businessEntityId": "0123456789", "taxId": "12-3456789", "website": "https://acme.example" },
    "registeredAddress": { "country": "US", "addressLine1": "1 Main St", "city": "New York", "state": "US-NY", "postalCode": "10001" },
    "operatingAddress": { "country": "US", "addressLine1": "1 Main St", "city": "New York", "state": "US-NY", "postalCode": "10001" },
    "companyClassification": { "legalStructure": "C-Corporation", "primaryIndustry": "manufacturing", "incorporationDate": "2020-01-15" },
    "ownership": {
      "persons": [
        {
          "roles": ["BENEFICIAL_OWNER", "CONTROLLING_PERSON"],
          "firstName": "Jane",
          "lastName": "Doe",
          "email": "jane@acme.example",
          "phoneNumber": "+14155550100",
          "birthDate": "1985-04-12",
          "nationality": "US",
          "taxIdType": "SSN",
          "taxIdNumber": "123-45-6789",
          "taxIdCountry": "US",
          "ownershipPercent": "100",
          "address": { "country": "US", "addressLine1": "1 Main St", "city": "New York", "state": "US-NY", "postalCode": "10001" },
          "documentIds": ["doc_2aProofOfAddrQb7vN4hL1"]
        }
      ]
    },
    "documentIds": ["doc_034A0gCCW6w2ubcipmRoY8"]
  }'
```

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`.

```json theme={null}
{
  "id": "app_034A0gCCVlvyhfAZssklTs",
  "clientReferenceId": "your-ref-001",
  "type": "customer_onboarding",
  "status": "processing",
  "reviewStage": "automated",
  "submittedAt": "2026-01-15T09:30:00.000Z",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "updatedAt": "2026-01-15T09:30:00.000Z"
}
```

<Note>
  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.
</Note>

<Warning>
  `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.
</Warning>

## 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](/webhooks).
* **Pulled** — fetch a fresh link on demand for any person:

```bash theme={null}
curl https://api.conduit.financial/v2/applications/{applicationId}/persons/{personReferenceId}/idv-link \
  -X POST \
  -H "x-api-key: YOUR_API_KEY"
```

```json theme={null}
{
  "url": "https://verify.example.com/verify?inquiry-id=inq_example&session-token=example",
  "shortUrl": "https://vfy.example.com/abc"
}
```

`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:

```bash theme={null}
curl https://api.conduit.financial/v2/applications/{applicationId} \
  -H "x-api-key: YOUR_API_KEY"
```

```json theme={null}
{
  "id": "app_034A0gCCVlvyhfAZssklTs",
  "type": "customer_onboarding",
  "status": "processing",
  "persons": [
    { "referenceId": "app_034A0gCCVlvyhfAZssklTs:a4K", "name": "Jane Doe" }
  ]
}
```

<Warning>
  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.
</Warning>

<Note>
  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.
</Note>

## 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.

```json theme={null}
{
  "applicationType": "customer_onboarding",
  "applicationId": "app_034A0gCCVlvyhfAZssklTs",
  "customerId": "cus_034A0gCCVsxdV2PjHLx9k1",
  "clientReferenceId": "your-ref-001"
}
```

`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](#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.

```json theme={null}
{
  "applicationType": "customer_onboarding",
  "applicationId": "app_034A0gCCVlvyhfAZssklTs",
  "failureCode": "rejected_by_ops",
  "failureMessage": "The uploaded proof of address is expired. Upload a current document and resubmit.",
  "resubmittable": true,
  "resubmissionGuidance": "Documents you already uploaded are still valid. Reuse the same documentIds in your new POST /v2/onboarding submission, and upload again only the documents you were asked to replace.",
  "clientReferenceId": "your-ref-001"
}
```

## Step 6 — Fetch the customer

On approval, retrieve the customer with the `customerId` from the webhook.

```bash theme={null}
curl https://api.conduit.financial/v2/customers/{customerId} \
  -H "x-api-key: YOUR_API_KEY"
```

## 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:

| `resubmittable` | `failureCode` | `failureMessage` | What to do |
| - | - | - | - |
| `true` | `rejected_by_ops` | The specific correction to make | Correct the submitted data and submit a fresh application. |
| `false` | `compliance_denied` | A fixed line: `"Your application requires additional review. Please contact support."` | The decision is final. Do not resubmit — direct the customer to support. |

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:

```bash theme={null}
curl https://api.conduit.financial/v2/applications/{applicationId} \
  -H "x-api-key: YOUR_API_KEY"
```

```json theme={null}
{
  "id": "app_034A0gCCVlvyhfAZssklTs",
  "clientReferenceId": "your-ref-001",
  "type": "customer_onboarding",
  "status": "rejected",
  "failureCode": "rejected_by_ops",
  "failureMessage": "The uploaded proof of address is expired. Upload a current document and resubmit.",
  "resubmittable": true,
  "resubmissionGuidance": "Documents you already uploaded are still valid. Reuse the same documentIds in your new POST /v2/onboarding submission, and upload again only the documents you were asked to replace.",
  "submittedAt": "2026-01-15T09:30:00.000Z",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "updatedAt": "2026-01-15T09:42:00.000Z"
}
```

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.

<Note>
  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.
</Note>

## 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`](/errors#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](/kyb/individuals)). 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

```bash theme={null}
curl 'https://api.conduit.financial/v2/onboarding/requirements?country=US&subjectType=individual' \
  -H "x-api-key: YOUR_API_KEY"
```

The [Individual quickstart](/sandbox/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](/kyb/individuals) 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.

```bash theme={null}
curl https://api.conduit.financial/v2/onboarding \
  -X POST \
  -H "x-api-key: YOUR_API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: 3b1e9d24-6a7c-4f2b-8e15-9d0c7a4b2e61" \
  -d '{
    "subjectType": "individual",
    "clientReferenceId": "your-ref-002",
    "ownership": {
      "persons": [
        {
          "roles": ["ACCOUNT_HOLDER"],
          "firstName": "Jane",
          "lastName": "Doe",
          "email": "jane@example.com",
          "phoneNumber": "+14155550100",
          "birthDate": "1985-04-12",
          "identityDocumentExpiryDate": "2031-04-12",
          "identityVerificationDate": "2026-10-01",
          "nationality": "US",
          "taxIdType": "SSN",
          "taxIdNumber": "123-45-6789",
          "taxIdCountry": "US",
          "address": { "country": "US", "addressLine1": "1 Main St", "city": "New York", "state": "US-NY", "postalCode": "10001" },
          "documentIds": ["doc_..."]
        }
      ]
    },
    "activity": {
      "occupation": "Software Engineer",
      "accountPurpose": ["Investment"],
      "sourceOfFunds": ["Salary"],
      "expectedMonthlyVolume": "VOLUME_10K_50K",
      "expectedTransactionsPerMonth": "10-50",
      "countriesOfActivity": ["USA"]
    },
    "regulatoryHistory": { "isPoliticallyExposedPerson": false },
    "certification": { "termsAndConditions": true }
  }'
```

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`](/errors#onboarding-already-submitted) with `details.applicationId` while this application is in review, and `409` [`CUSTOMER_ALREADY_ONBOARDED`](/errors#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](/kyb/individuals#phone-number). 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](#step-5--listen-for-the-result). For everything a person owes, see [Onboarding an Individual](/kyb/individuals). Once approved, [Get a customer by ID](/api-reference/customers/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](/api-reference/customers/submit-a-customer-update-application); 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](/kyb/individuals#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](/guides/add-virtual-accounts).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.