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

# Customer KYC (Sandbox)

> How customer KYC is mocked in the sandbox environment, and how to drive specific outcomes on any application

## Overview

In the sandbox environment, customer KYC is **mocked**: no upstream KYC or screening provider is called. Customer onboarding still goes through the same review as production. Only the provider answers are local: a business's KYC checks clear after one hour, and an individual's sanctions screen clears at once, as it does for a clean person on live. The document review is not local. Sandbox reads your documents as live does, with one difference: it does not check if a person's ID document is authentic, because a sandbox person has no real ID. A sample file that reads as a passport, national ID card or driver's licence counts as the person's ID.

Your organization does KYB in production. Your sandbox organization is ready at once.

## Customer-onboarding lifecycle

### Business

When you submit a business customer onboarding application in sandbox:

1. The application enters `"processing"`.
2. The mock marks the same fields pending that a live KYC check would (business website, classification report, adverse-media report, associated persons, TIN — depending on the submitted data and country).
3. A 1-hour timer starts. When it fires, the mock KYC checks clear. The timer does not approve the application.
4. Call one of the simulate endpoints below to approve or reject the application. An undecided application does not wait forever: it cancels automatically after 7 days. Watch for `reviewStage: "operator"` on the application — that is the signal to call `simulate/decision`.

### Individual

An individual onboards under KYC reliance, so sandbox decides it the same way live does:

1. The application enters `"processing"`.
2. The account holder's sanctions screen clears. The one exception is an account holder with the first name `Ivan` and the last name `Sanctionov`: the screen finds a sanctions match, so the application stays `"processing"`, as a match waits for Conduit compliance on live. Decide it with the simulate endpoint below.
3. The pipeline compares the identity verification attestation you uploaded with the person you submitted, and checks that you attached the original ID document.
4. If the attestation names the same person and shows a labelled verification date, for example `Verification date: 2026-10-01`, and the ID document is attached, the application is `approved` with no call from you, usually within a few minutes of the submit. Listen for the webhook rather than poll on a short timer. `application.approved` and `customer.created` fire.
5. If the attestation does not match, for example a different name or birth date, a verification date older than one year or no readable verification date, a name that looks like a test marker such as `ZZZTEST`, a file that is not an attestation, or a missing ID document, the application stays `"processing"`, as it waits for a Conduit operator on live. Drive it with the simulate endpoint below.

To test a rejection, upload an attestation that does not match the person, then reject the application with the simulate endpoint.

The first decision wins. A second call with a different outcome returns `409 APPLICATION_ALREADY_DECIDED`.

## Simulate endpoints

`POST /v2/sandbox/applications/:id/simulate/decision` forces a sandbox application to a terminal status on demand, regardless of the application's current review stage. The body's `outcome` field selects `approved` or `rejected`.

### Works for any application type

This endpoint is not limited to customer onboarding. It accepts every application type your sandbox API key can create:

| Application type | On `outcome: "approved"` |
| - | - |
| `customer_onboarding` | Customer transitions to `active`; `application.approved` and `customer.created` fire. |
| `virtual_account` | The virtual account and its underlying account details are provisioned in one shot (see [Virtual accounts](/concepts/virtual-accounts) for the end-to-end shape). |
| `crypto_wallet` | The crypto-wallets feature is enabled for the customer. The account itself is provisioned by a separate wallet call — see [Wallet provisioning](#wallet-provisioning) below. |
| `customer_update` | The pending customer update is applied. |

`outcome: "rejected"` publishes the matching `*.rejected` event for the application's type. Rejected applications never reach a provisioning branch.

### Request shape

The endpoint requires your sandbox API key and a JSON body with an `outcome` field. The simplest approval call:

```bash theme={null}
curl -X POST https://api.sandbox.conduit.financial/v2/sandbox/applications/app_01H.../simulate/decision \
  -H "x-api-key: ck_sandbox_..." \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "approved" }'
```

Returns `200 OK` with the application at its new state. A call with the outcome the application already has returns `200` with the application unchanged, so a call made after an individual auto-approves still succeeds. A call with the other outcome returns `409 APPLICATION_ALREADY_DECIDED`. A call that arrives while the application is being decided can return `409 APPLICATION_INVALID_STATUS`. Read the application to see its decision.

### Rejection options

The same endpoint with `outcome: "rejected"` accepts optional fields to shape the rejection:

| Body | Behavior |
| - | - |
| `{ "outcome": "rejected" }` | Generic rejection. The persisted reason is a default message. |
| `{ "outcome": "rejected", "reason": "<text>" }` | Free-text rejection. The string is persisted verbatim as the rejection reason. |
| `{ "outcome": "rejected", "category": "...", "field": "..." }` | Structured rejection that drives the rejection down a specific sub-path so the persisted reason matches what the live KYB pipeline would produce. |

Structured `category` is only meaningful for the KYB-pipeline application types your sandbox API key can create: `customer_onboarding` and `customer_update`. Sending `category` on a non-KYB type (e.g. a `virtual_account` application) returns `422 REJECTION_CATEGORY_NOT_APPLICABLE`. Sending `category` with `outcome: "approved"` returns `400 VALIDATION_ERROR`. Within `category: generic` an optional `reason` is accepted (this is how integrators simulate a manual operator rejection); for all other categories `reason` cannot be combined with `category`.

Categories: `document_mismatch` (optional `field`: `tax_id`, `date_of_incorporation`, `business_name`, `business_entity_id`, and for an individual application's own fields `account_holder_birth_date`, `account_holder_address`, `account_holder_tax_id`), `data_sourcing_mismatch` (optional `field`: `business_entity_id`, `ubo_first_name`, `ubo_last_name`, `ubo_phone`, `ubo_email`, `ownership_list`; on an individual application the `ubo_*` fields point at the account holder), `compliance` (no `field`), `generic` (no `field`; accepts an optional `reason`). The `category` value is not case-sensitive.

To run an individual application from discovery to a payout, see the [Individual quickstart](/sandbox/individual-quickstart). On an individual application, `failureMessage` names the account holder where a business application names an owner, for example "The account holder's phone number couldn't be matched against our records."

## Errors

| Status | Reason |
| - | - |
| `400` | Missing `outcome`, non-object body, or `reason` combined with a `category` other than `generic`. Also returned when `category` is sent with `outcome: "approved"`. |
| `401` | Missing or invalid `x-api-key`. |
| `404` | The application does not exist or does not belong to your organization. |
| `409` | `APPLICATION_ALREADY_DECIDED`: the application is already `approved` or `rejected`, and the call asks for the other outcome. `APPLICATION_INVALID_STATUS`: the application got its decision while the call was in progress. |
| `422` | `category` was sent against a non-KYB application type (`virtual_account`, `crypto_wallet`). |

## Wallet provisioning

Submitting the `crypto_wallet` feature application alone enables the feature but does not provision an account.

For a non-custodial organization, the customer's wallets are provisioned via `POST /v2/customers/:id/wallets/claim-non-custodial`, which mints a non-custodial wallet account, sets up the signer roster, and creates the wallets in one shot. See the [Multi-signer wallets recipe](/sandbox/multi-signer-wallets) for the full copy-paste curl flow.

For an organization enabled for custodial wallets, no claim is required — the account is provisioned on the customer's first `POST /v2/customers/:id/wallets` call.

## Notes

* Customer KYC data submitted to sandbox is **not** transferred to production. Sandbox and production are isolated environments — end-user customers re-onboard in production.
* The mock produces the same verification shape your client code sees in production, whether it's running against sandbox or live.
* For happy-path tests you can ignore simulation: submit, wait, then observe `approved`. A business `customer_onboarding` application does not auto-approve: call `simulate/decision`. An individual whose attestation matches and shows a verification date, with the ID document attached, approves with no call, usually within a few minutes. `virtual_account` and `crypto_wallet` applications usually approve at submission too, with no call needed. `customer_update` does not auto-resolve: drive it with `simulate/decision { outcome: "approved" | "rejected" }`.

## See also

* [Sandbox overview](/sandbox/overview)
* [Webhooks reference](/webhooks)
* [Error codes](/errors)
* [Sandbox quickstart](/sandbox/quickstart)
* [Individual quickstart](/sandbox/individual-quickstart)
* [Virtual accounts](/concepts/virtual-accounts)
* [Non-Custodial Wallets](/concepts/non-custodial-wallets)
* [Deposits](/sandbox/deposits)
* [Withdrawals](/sandbox/withdrawals)


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