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

# KYC Verification

> Submit and track Know Your Customer verification for workers

export const ApiExample = ({method = "GET", path, params, body, reference, tenantKey}) => {
  const baseUrl = "https://api.cadanapay.com";
  const query = params ? "?" + Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&") : "";
  const url = `${baseUrl}${path}${query}`;
  const isPlatformPath = (/^\/(v1\/)?platform(\/|$)/).test(path || "");
  const includeTenantKey = tenantKey === undefined ? !isPlatformPath : tenantKey;
  let curl = `curl -X ${method} '${url}' \\\n  -H 'Authorization: Bearer YOUR_API_KEY'`;
  if (includeTenantKey) {
    curl += ` \\\n  -H 'X-MultiTenantKey: YOUR_BUSINESS_TENANT_KEY'`;
  }
  if (body) {
    const formatted = JSON.stringify(body, null, 2);
    curl += ` \\\n  -H 'Content-Type: application/json' \\\n  -d '${formatted}'`;
  }
  return <div>
      <CodeBlock language="bash" filename="bash" wrap>
        {curl}
      </CodeBlock>
      {reference && <div style={{
    marginTop: "-0.5rem",
    marginBottom: "1rem"
  }}>
          <a href={reference} style={{
    fontSize: "0.875rem"
  }}>
            Try it in the playground →
          </a>
        </div>}
    </div>;
};

Know Your Customer (KYC) verification is required before a user can receive payments through Cadana. You submit identity documents and personal information via the API, and Cadana handles automated verification. This page covers the submission requirements, status tracking, and webhook events.

***

## KYC Flow

```mermaid theme={null}
flowchart LR
    A[not started] -->|Submit KYC| B[processing]
    B -->|Auto-verified| C[approved]
    B -->|Failed| D[rejected]
    D -->|Resubmit| B
```

| Status        | Meaning                                       | User can transact? |
| :------------ | :-------------------------------------------- | :----------------- |
| `not started` | KYC has not been submitted                    | No                 |
| `processing`  | Documents submitted, verification in progress | No                 |
| `approved`    | Verification successful                       | Yes                |
| `rejected`    | Verification failed — resubmission needed     | No                 |

You may also see `pending` while verification is underway — treat it like `processing` and wait for `approved` or `rejected`.

Most verifications complete automatically within minutes. If automated checks can't verify the user's identity, a manual review may take 1-2 business days.

***

## Submit KYC

Submit identity information and documents for a user. Upload document images first using the [file upload flow](/reference/working-with-files), then pass the returned `fileId` values.

<ApiExample method="POST" path="/v1/users/8ef9a712-cdae-4110-b1ea-9ba95abbee6e/kyc" body={{ firstName: "John", lastName: "Doe", dob: "1990-01-15", nationality: "US", idDetails: { type: "PASSPORT", number: "123456789", issuedBy: "U.S. Department of State", issuedDate: "2020-01-15", expirationDate: "2030-01-15", issuedCountryCode: "US", frontFileId: "550e8400-e29b-41d4-a716-446655440000", selfieFileId: "550e8400-e29b-41d4-a716-446655440001" }, address: { line1: "123 Main Street", line2: "Apt 4B", city: "San Francisco", state: "CA", postalCode: "94102", countryCode: "US" } }} reference="/api-reference/workforce/users/submit-kyc" />

Returns `204` on success. The user's KYC status moves to `processing`.

### Personal Information

| Field         | API Field     | Required | Rules                                              |
| :------------ | :------------ | :------- | :------------------------------------------------- |
| First name    | `firstName`   | Yes      | Must match government-issued ID exactly            |
| Last name     | `lastName`    | Yes      | Must match government-issued ID exactly            |
| Date of birth | `dob`         | Yes      | Format: `YYYY-MM-DD`                               |
| Nationality   | `nationality` | Yes      | 2-letter ISO country code (e.g., `US`, `NG`, `KE`) |

### Identity Document

| Field             | API Field                     | Required | Rules                                                      |
| :---------------- | :---------------------------- | :------- | :--------------------------------------------------------- |
| Document type     | `idDetails.type`              | Yes      | See supported types below                                  |
| Document number   | `idDetails.number`            | Yes      | Unique identifier on the document                          |
| Issuing authority | `idDetails.issuedBy`          | Yes      | Authority that issued the document                         |
| Issue date        | `idDetails.issuedDate`        | Yes      | Format: `YYYY-MM-DD`                                       |
| Expiration date   | `idDetails.expirationDate`    | Yes      | Format: `YYYY-MM-DD`. Must not be expired                  |
| Issuing country   | `idDetails.issuedCountryCode` | Yes      | 2-letter ISO country code                                  |
| Front of ID       | `idDetails.frontFileId`       | Yes      | `fileId` from [file upload](/reference/working-with-files) |
| Back of ID        | `idDetails.backFileId`        | No       | Required for driver's licenses and some national IDs       |
| Selfie            | `idDetails.selfieFileId`      | Yes      | Photo of user holding the document                         |

**Supported document types:**

| Document         | API Value        |
| :--------------- | :--------------- |
| Passport         | `passport`       |
| Driver's license | `driver_license` |
| National ID card | `national_id`    |
| ID card          | `id_card`        |
| Voter ID card    | `voter_id_card`  |
| PAN card         | `pan_card`       |

<Note>
  Images must be clear and readable with all text visible. Supported formats: JPEG, PNG.
</Note>

### Address (Optional)

If provided, all required address fields must be completed.

| Field            | API Field             | Required                |
| :--------------- | :-------------------- | :---------------------- |
| Street address   | `address.line1`       | Yes                     |
| Address line 2   | `address.line2`       | No                      |
| City             | `address.city`        | Yes                     |
| State / Province | `address.state`       | Yes                     |
| Postal code      | `address.postalCode`  | Yes                     |
| Country          | `address.countryCode` | Yes (2-letter ISO code) |

An address proof document can be provided via `addressProofFileId` (utility bill, bank statement, or government document dated within the last 3 months).

***

## Check KYC Status

Retrieve a user's current KYC status.

<ApiExample method="GET" path="/v1/users/8ef9a712-cdae-4110-b1ea-9ba95abbee6e/kyc" reference="/api-reference/workforce/users/get-kyc" />

**Response:**

```json theme={null}
{
  "userKyc": {
    "firstName": "John",
    "lastName": "Doe",
    "identity": "approved",
    "address": "not started",
    "idDetails": {
      "country": "US",
      "type": "Drivers license"
    }
  }
}
```

Identity and address verification are tracked separately. A user needs at minimum `identity: "approved"` to receive payments. If a verification is rejected, the `identityStatusReason` or `addressStatusReason` field explains why.

***

## Resubmit KYC

If a user's identity or address verification is rejected, use the PATCH endpoint to resubmit corrected information. You can resubmit identity, address, or both in a single request.

**Resubmission rules:**

| Section  | Can resubmit when                     |
| :------- | :------------------------------------ |
| Identity | Status is `rejected`                  |
| Address  | Status is `not started` or `rejected` |

<Note>
  At least one of `identity` or `address` must be provided. When a section is included, all of its fields are required.
</Note>

### Resubmit identity only

<ApiExample method="PATCH" path="/v1/users/8ef9a712-cdae-4110-b1ea-9ba95abbee6e/kyc" body={{ identity: { firstName: "John", lastName: "Doe", dob: "1990-01-15", nationality: "US", idDetails: { type: "passport", number: "123456789", issuedBy: "U.S. Department of State", issuedDate: "2020-01-15", expirationDate: "2030-01-15", issuedCountryCode: "US", frontFileId: "550e8400-e29b-41d4-a716-446655440000", backFileId: "550e8400-e29b-41d4-a716-446655440001", selfieFileId: "550e8400-e29b-41d4-a716-446655440002" } } }} reference="/api-reference/workforce/users/resubmit-kyc" />

### Resubmit address only

<ApiExample method="PATCH" path="/v1/users/8ef9a712-cdae-4110-b1ea-9ba95abbee6e/kyc" body={{ address: { address: { line1: "123 Main Street", line2: "Apt 4B", city: "San Francisco", state: "CA", postalCode: "94102", countryCode: "US" }, addressProofFileId: "550e8400-e29b-41d4-a716-446655440003" } }} reference="/api-reference/workforce/users/resubmit-kyc" />

Returns `204` on success. Only the resubmitted section(s) move to `processing` — approved sections remain unchanged.

***

## Webhook Events

### user.kyc.updated

Fired on every KYC status change — processing, approved, or rejected. The `type` field indicates which verification component changed.

```json theme={null}
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "eventType": "user.kyc.updated",
  "version": "1.0",
  "timestamp": 1765359752,
  "data": {
    "userId": "8ef9a712-cdae-4110-b1ea-9ba95abbee6e",
    "status": "approved",
    "type": "identity",
    "tenantKey": "cad95193904"
  }
}
```

| Field    | Description                                         |
| :------- | :-------------------------------------------------- |
| `userId` | The user whose KYC status changed                   |
| `status` | New status: `processing`, `approved`, or `rejected` |
| `type`   | Verification component: `identity` or `address`     |

For rejected verifications, fetch the user's KYC details with [`GET /v1/users/{userId}/kyc`](/api-reference/workforce/users/get-kyc) to see the failure reason.

### user.kyc.expiry

Fired when a user's identity document is approaching expiry or has expired.

```json theme={null}
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "eventType": "user.kyc.expiry",
  "version": "1.0",
  "timestamp": 1765359752,
  "data": {
    "userId": "8ef9a712-cdae-4110-b1ea-9ba95abbee6e",
    "stage": "about-to-expire",
    "documentType": "passport",
    "expiryDate": "2026-03-15",
    "tenantKey": "cad95193904"
  }
}
```

| Stage             | Meaning                                                                                             |
| :---------------- | :-------------------------------------------------------------------------------------------------- |
| `about-to-expire` | Document expires within 30 days                                                                     |
| `in-grace-period` | Expired but within 60-day grace period — existing transactions continue, new ones may be restricted |
| `expired`         | Past grace period — user must re-verify                                                             |

See [Events](/reference/events) for all event types and payload details.

***

## Handling Rejections

When KYC is rejected, the user needs to resubmit. Common rejection reasons:

| Reason               | User action                                    |
| :------------------- | :--------------------------------------------- |
| Document expired     | Submit valid, non-expired document             |
| Image unreadable     | Retake photo with good lighting                |
| Document damaged     | Submit undamaged document                      |
| Info mismatch        | Verify name and DOB match the document exactly |
| Unsupported document | Submit an accepted document type               |

To resubmit, call [`PATCH /v1/users/{userId}/kyc`](/api-reference/workforce/users/resubmit-kyc) with corrected information for the rejected section(s). The status resets to `processing`.

***

## Sandbox Testing

Use sentinel values to simulate KYC approval or rejection in the sandbox environment without waiting for real verification. These sentinels work with both initial submission (`POST`) and resubmission (`PATCH`).

<Warning>
  In sandbox, always use a sentinel. A submission without one is never resolved, and because resubmission requires a `rejected` status, the user cannot complete KYC — recover by [deleting the sandbox user](/reference/sandbox#resetting-sandbox-users) and re-inviting.
</Warning>

### Identity Verification

Set `idDetails.number` to a sentinel value:

| Test Value     | Field              | Result                          |
| :------------- | :----------------- | :------------------------------ |
| `auto-approve` | `idDetails.number` | Identity automatically approved |
| `auto-reject`  | `idDetails.number` | Identity automatically rejected |

### Address Verification

Set `address.line2` to a sentinel value:

| Test Value     | Field           | Result                         |
| :------------- | :-------------- | :----------------------------- |
| `auto-approve` | `address.line2` | Address automatically approved |
| `auto-reject`  | `address.line2` | Address automatically rejected |

You can combine both sentinels in a single request to auto-resolve identity and address independently. For example, `idDetails.number: "auto-approve"` with `address.line2: "auto-reject"` will approve identity and reject address.

<Note>
  When a sentinel is used, the document file IDs (`frontFileId`, `selfieFileId`, `addressProofFileId`) can be omitted — no file upload is needed. If you do pass them, they must reference files you actually [uploaded](/reference/working-with-files); made-up IDs are rejected with a `404`. Without a sentinel, file IDs are required, as in production.
</Note>

### Testing the Resubmission Flow

To test the full rejection-to-resubmission cycle in sandbox:

1. Submit KYC with `idDetails.number: "auto-reject"` to get a rejected identity
2. Resubmit via PATCH with `idDetails.number: "auto-approve"` to approve it

<ApiExample method="PATCH" path="/v1/users/{userId}/kyc" body={{ identity: { firstName: "John", lastName: "Doe", dob: "1990-01-15", nationality: "US", idDetails: { type: "passport", number: "auto-approve", issuedBy: "U.S. Department of State", issuedDate: "2020-01-15", expirationDate: "2030-01-15", issuedCountryCode: "US", frontFileId: "550e8400-e29b-41d4-a716-446655440000", selfieFileId: "550e8400-e29b-41d4-a716-446655440001" } } }} reference="/api-reference/workforce/users/resubmit-kyc" />

<Warning>
  These test values only work in the sandbox environment and are silently ignored in production. Sentinel values are case-insensitive.
</Warning>

See [Sandbox & Testing](/reference/sandbox#kyc-testing) for all test values.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Working with Files" icon="file-arrow-up" href="/reference/working-with-files">
    Upload identity documents before submitting KYC
  </Card>

  <Card title="KYB Requirements" icon="building" href="/platform/kyb-requirements">
    Business verification requirements
  </Card>

  <Card title="Onboard Workers" icon="user-plus" href="/workforce/onboarding-workers">
    Create Person and User records
  </Card>

  <Card title="Sandbox & Testing" icon="flask-vial" href="/reference/sandbox">
    Test values and simulated scenarios
  </Card>
</CardGroup>
