> ## Documentation Index
> Fetch the complete documentation index at: https://v2.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 provider is called, and no end-user KYC data is collected. Customer onboarding still flows through the full review pipeline (sourcing, document validation, extraction, verification, compliance checks) — but the compliance-checks stage is satisfied by a mock that defers the verdict for one hour by default.

Org-level KYB is **not** mocked. Your organization completes real KYB against real providers in sandbox; sandbox KYB approval is the prerequisite for graduating to production.

## Customer-onboarding lifecycle

When you submit a customer onboarding application in sandbox:

1. The application enters `"processing"`.
2. The mock records `pending` field verifications for the same paths a real KYC transaction would (business website, classification report, adverse-media report, associated persons, TIN — depending on the submitted data and country).
3. A 1-hour timer starts.
4. **Either** you call one of the simulate endpoints below to drive a specific outcome, **or** the timer fires and auto-approves the application.

First call wins: subsequent simulate calls return `409 Conflict`.

## 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` and `customer.activated` fires.                                                                                                                                                                          |
| `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 only after a separate `POST /v2/customers/:id/wallets/claim-non-custodial` call (see [Non-Custodial Wallets](/concepts/non-custodial-wallets)). |
| `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 current state. Replays against an already-terminal application return `200` with the current resource (idempotent).

### Rejection options

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

| Body                                                           | Behavior                                                                                                                                          |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{ "outcome": "rejected" }`                                    | Generic rejection. The persisted reason falls back to a sentinel string.                                                                          |
| `{ "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 KYB-pipeline application types (`customer_onboarding`, `organization_onboarding`, `customer_update`, `sanctions_review`). 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`), `data_sourcing_mismatch` (optional `field`: `business_entity_id`, `ubo_first_name`, `ubo_last_name`, `ubo_phone`, `ubo_email`, `ownership_list`), `compliance` (no `field`), `generic` (no `field`; accepts an optional `reason`).

## 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.                                                                                            |
| `422`  | `category` was sent against a non-KYB application type (`virtual_account`, `crypto_wallet`).                                                                       |

## Non-custodial wallet provisioning

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. Submitting the `crypto_wallet` feature application alone enables the feature row but does not provision an account.

See the [Custodial vs non-custodial guide](/sandbox/custody) for the full copy-paste curl flow.

## 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 matches the real KYC adapter's field-path set so your client code sees the same shape on `application_field_verifications` whether it's running against sandbox or live.
* The 1-hour fallback applies to customer-onboarding applications only and makes it safe to ignore simulation entirely for happy-path tests — submit, wait, observe an `approved` outcome. Other application types do not auto-resolve; drive them with `simulate/decision { outcome: "approved" | "rejected" }`.

## See also

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