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

# Sandbox & Testing

> Testing your integration in the sandbox environment

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>;
};

Cadana's sandbox environment lets you test your integration without processing real financial transactions. The sandbox APIs mirror production, so transitioning to live is as simple as updating the host and API key.

**Sandbox host:** `https://dev-api.cadanapay.com`

<Warning>
  Use an API key generated for the sandbox environment. Production API keys will not work with the sandbox host.
</Warning>

<Warning>
  The sandbox sends **real emails** — user invites, contract signature requests, and other notifications are delivered to the addresses you provide, and user accounts are real login accounts. Use inboxes you control (e.g., plus-addressing on your own domain), and pass `suppressWelcomeEmail` / `suppressNotification` where available.
</Warning>

***

## Funding Your Wallet

Use the sandbox deposit endpoint to add test funds to your wallet:

<ApiExample method="POST" path="/v1/sandbox/business-deposits" body={{ value: { amount: 10000000, currency: "USD" }, description: "Test funding" }} reference="/api-reference/workforce/sandbox/deposit" />

***

## Testing Payrolls

To run a payroll through its full lifecycle without waiting, save it with a `payrollDate` in the past — it completes immediately on approval. This is the recommended way to test [payslip downloads](/workforce/pay-workers-via-payroll#downloading-payslips) end-to-end, since payslips exist only for `Completed` payrolls. Payrolls with a future `payrollDate` disburse on that date.

***

## Simulated Payment Failures

To test your error handling, use specific `accountName` values when creating a beneficiary to trigger simulated payment failures:

| Account Name      | Simulated Failure      |
| :---------------- | :--------------------- |
| `FAILURE_SIM_001` | Invalid account        |
| `FAILURE_SIM_002` | Bank unavailable       |
| `FAILURE_SIM_003` | Rejected by compliance |
| `FAILURE_SIM_004` | Returned by bank       |

Set the `accountName` in the beneficiary's bank payment details:

<ApiExample method="POST" path="/v1/beneficiaries" body={{ name: "John Doe", countryCode: "MX", currency: "MXN", paymentDetails: { preferredMethod: "bank", bank: { accountName: "FAILURE_SIM_003", accountNumber: "0123456789", bankCode: "014", bankName: "Example Bank" } }, kyc: { firstName: "John", lastName: "Doe", dateOfBirth: "1990-01-15", countryCode: "MX", idType: "PASSPORT", idNumber: "AB1234567", idFileFront: "file-id" } }} />

When the payout is processed, it will fail with the corresponding error message — in this case, "Rejected by compliance".

***

## Virtual Account Auto-Approval

When creating a virtual account in sandbox, use `99999900` as the `customIdentification.number` to automatically approve the account. The `type` (SSN or BVN) does not matter.

<ApiExample method="POST" path="/v1/users/userId/virtual-accounts" body={{ currency: "USD", customIdentification: { type: "ssn", number: "99999900" }, jobInformation: { employerName: "Acme Inc", address: { line1: "123 Main St", city: "Anytown", postalCode: "12345", state: "CA", countryCode: "US" }, title: "Software Engineer", type: "full-time", netPay: { amount: 10000, currency: "USD" }, frequency: "monthly", contractStartDate: "2021-01-01", contractEndDate: "2025-05-06" }, suppressNotification: true }} />

***

## KYC Testing

Use these test values when submitting KYC in sandbox to simulate different verification outcomes.

### Identity Document Number

| Value          | Result                                       |
| :------------- | :------------------------------------------- |
| `auto-approve` | Identity verification automatically approved |
| `auto-reject`  | Identity verification automatically rejected |

### Address Line 2

| Value          | Result                                      |
| :------------- | :------------------------------------------ |
| `auto-approve` | Address verification automatically approved |
| `auto-reject`  | Address verification automatically rejected |

Identity and address sentinels can be combined in a single request to control each verification independently. Sentinel values are case-insensitive.

These sentinels work with both initial submission (`POST`) and resubmission (`PATCH`).

<Note>
  The examples below include document file IDs to show the full request shape. When a sentinel is used, `frontFileId`, `selfieFileId`, and `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`.
</Note>

<Warning>
  Always use a sentinel when testing KYC in sandbox. A submission without one is **not** auto-approved — it stays unresolved, and since resubmission is only allowed after a rejection, the user cannot complete KYC. To recover, [delete the sandbox user](#resetting-sandbox-users) and re-invite. Without a sentinel, document file IDs are also required, as in production.
</Warning>

**Initial submission:**

<ApiExample method="POST" path="/v1/users/{userId}/kyc" body={{ 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" }, address: { line1: "123 Main Street", line2: "auto-approve", city: "San Francisco", state: "CA", postalCode: "94102", countryCode: "US" }, addressProofFileId: "550e8400-e29b-41d4-a716-446655440002" }} />

**Resubmission (after rejection):**

<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" } } }} />

***

## Resetting Sandbox Users

Delete a sandbox user to start over — for example, after a KYC submission that can't be resubmitted. The linked Person record is preserved, so you can re-invite the same person with [`POST /v1/users/invite`](/api-reference/workforce/users/invite).

<ApiExample method="DELETE" path="/v1/sandbox/users/{userId}" reference="/api-reference/workforce/sandbox/delete-user" />

Returns `204` on success. This endpoint only exists in the sandbox environment.

***

## KYB Testing

Use these test values when submitting KYB information in sandbox to simulate different outcomes.

### Tax Identification Number

| Tax ID                 | Result                                                                                                |
| :--------------------- | :---------------------------------------------------------------------------------------------------- |
| `000-CAD-AUTO-APPROVE` | KYB automatically approved                                                                            |
| `000-CAD-AUTO-REJECT`  | KYB automatically rejected (terminal)                                                                 |
| `000-CAD-AUTO-FLAG`    | Status set to `needs-additional-info` — flags tax ID as incorrect and requests all business documents |

### Principal SSN

| SSN           | Result                                                                                                         |
| :------------ | :------------------------------------------------------------------------------------------------------------- |
| `000-11-2222` | Status set to `needs-additional-info` — requires identity (front and back) and address files for the principal |
| `000-11-3333` | Status set to `needs-additional-info` — principal receives email to complete KYC via provider                  |

<ApiExample method="POST" path="/v1/businesses/{businessId}/kyb" body={{ entityName: "Acme Inc", entityType: "corporation", taxIdentificationNumber: "000-CAD-AUTO-APPROVE" }} />

<Note>
  You can't combine `000-CAD-AUTO-APPROVE` with a sentinel SSN in the same submission — the request returns `400 cannot combine auto-approve tax ID with auto-feedback principal presets`.
</Note>

***

<Warning>
  All sandbox test values are ignored silently in production.
</Warning>
