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

# Testing Your Integration

This guide covers how to test your Dakota Platform integration in our sandbox environment.

# Sandbox Philosophy

Dakota's sandbox runs **real crypto custody on testnets** and **mocks the banking layer**. Wallets sign and broadcast real testnet transactions; the policy engine runs unmodified. Fiat rails (ACH, Fedwire) are mocked.

What that means per flow:

* **Onramp** — `POST /sandbox/simulate/inbound` mocks the USD deposit. The follow-on stablecoin transfer to the Destination is a real testnet transaction.
* **Offramp** — send real testnet crypto to the offramp's `source_crypto_address`. The on-chain inbound is real; the USD outbound to the bank Destination is mocked.
* **Swap** — fully on-chain on testnets, end to end.

Onboarding lifecycle (KYB / KYC) is driven by `POST /sandbox/simulate/onboarding` instead of waiting for real compliance review.

* **Base URL:** `https://api.platform.sandbox.dakota.xyz`
* **Dashboard:** `https://platform.sandbox.dakota.xyz`
* **API keys:** Base64-encoded strings, generated from the dashboard.

# Sandbox Restrictions

Two safeguards run on every object-create request in sandbox to prevent accidental mainnet activity and unbounded test spend.

## Network Restriction

Object-create endpoints — `POST /customers/{id}/recipients`, `POST /recipients/{id}/destinations`, `POST /accounts`, and `POST /transactions/one-off` — reject mainnet networks in sandbox and reject testnet networks in production. The `evm` wildcard is allowed in every environment.

| Environment | Accepted                                                                                                                          | Rejected |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- |
| Sandbox     | testnets (`ethereum-sepolia`, `polygon-amoy`, `arbitrum-sepolia`, `base-sepolia`, `optimism-sepolia`, `solana-devnet`), `evm`     | mainnets |
| Production  | mainnets (`ethereum-mainnet`, `polygon-mainnet`, `arbitrum-mainnet`, `base-mainnet`, `optimism-mainnet`, `solana-mainnet`), `evm` | testnets |

A request that violates the rule returns `400 Bad Request`. Use the testnet equivalent for the chain you're targeting.

## Amount Cap

Per-transaction amounts in sandbox are capped at **\$2 USD** per request. The cap applies to USD, USDC, and RD (all treated 1:1 to USD). USDT is not supported in sandbox at the moment — it will be enabled in a near-future release. Production has no cap and supports the full asset set.

The cap applies at:

* `POST /transactions/one-off` — the `amount` field. Rejected synchronously with `400 Bad Request`.
* `POST /sandbox/simulate/inbound` — the `amount` field. Rejected synchronously with `400 Bad Request`.
* **Auto-account inbound deposits** — when a sandbox crypto deposit lands on an auto-account's source address and the converted USD value exceeds the cap, the auto-account transaction is created and then transitioned to `failed` with a `failure_reason` indicating the cap was exceeded. The failure surfaces via the standard `transaction.auto.updated` webhook (and on `GET /auto-transactions/{id}`); there is no synchronous rejection because the deposit has already settled on-chain.

Use small values (`"1.00"`, `"2.00"`) in your sandbox test fixtures.

# Simulation Endpoints

## Simulate an Inbound Payment

`POST /sandbox/simulate/inbound` injects an inbound USD payment event (ACH or Fedwire) against an onramp account, mocking what would normally arrive from a real bank wire. The call is accepted synchronously; webhook callbacks fire asynchronously and match the production event shape exactly. Crypto deposits aren't simulated — send real testnet crypto to the offramp/swap/one-off `source_crypto_address` directly.

```bash theme={null}
# Inbound ACH against an onramp account
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/simulate/inbound \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "simulation_id": "sim_onramp_001",
    "type": "ach_inbound",
    "account_id": "2LfZn6LNoSvMGuSQ0pLLxj6BLGR",
    "amount": "1.00",
    "currency": "USD",
    "scenario": "success_immediate"
  }'
```

Key fields:

* `type` — `ach_inbound` or `wire_inbound` for USD rails.
* `account_id` — required for `ach_inbound` and `wire_inbound`. Use the onramp account ID.
* `scenario` — `success_immediate` (default) or `success_delayed` with a `delay_seconds` field.
* `simulation_id` — your idempotency key for the simulation. Repeating the call with the same ID and identical params returns the original response; conflicting params return `409`.

The full schema and the catalog of available scenarios live in the API reference: [Simulate an Inbound Payment Event](/api-reference/sandbox/simulate-an-inbound-payment-event), [List Simulation Scenarios](/api-reference/sandbox/list-available-simulation-scenarios).

<Warning>
  Sandbox only — production returns `403 Forbidden`.
</Warning>

## Simulate an Onboarding Transition

`POST /sandbox/simulate/onboarding` drives KYB or KYC application status through a sandbox transition without waiting for real compliance review.

```bash theme={null}
# Approve a customer's KYB
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/simulate/onboarding \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "kyb_approve",
    "applicant_id": "01HABCDEFG1234567890XYZ",
    "simulation_id": "sim_kyb_approve_001"
  }'
```

```bash theme={null}
# Reject KYB with a reason code
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/simulate/onboarding \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "kyb_reject",
    "applicant_id": "01HABCDEFG1234567890XYZ",
    "simulation_id": "sim_kyb_reject_001",
    "reason_code": "MISSING_EIN"
  }'
```

```bash theme={null}
# Request additional information
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/simulate/onboarding \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "kyb_info_request",
    "applicant_id": "01HABCDEFG1234567890XYZ",
    "simulation_id": "sim_kyb_info_001",
    "info_request_fields": ["ssn", "address"]
  }'
```

`type` values:

| `type`                                          | Effect                                                                                                                               |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `kyb_approve`                                   | Fully approves the customer (any type). Triggers endorsement, recipient creation, and `customer.kyb_status.created` webhooks.        |
| `kyb_reject`                                    | Sets application status to `declined`.                                                                                               |
| `kyb_info_request`                              | Sets application status to `request_for_information`.                                                                                |
| `kyc_approve`, `kyc_reject`, `kyc_info_request` | Updates the individual applicant's KYC status only. Does not trigger the full onboarding flow.                                       |
| `applicant_activate`                            | Same effect as `kyb_approve`. Does not auto-create accounts, wallets, or account numbers — create those separately after activation. |
| `applicant_suspend`                             | Sets application status to `declined`.                                                                                               |

Full schema: [Simulate an Onboarding State Transition](/api-reference/sandbox/simulate-onboarding).

<Warning>
  Sandbox only — production returns `403 Forbidden`.
</Warning>

## Tracking Simulation State

Stateful scenarios (e.g. `compliance_hold`, `manual_review`, `unconfirmed`) pause partway through the lifecycle and wait for an explicit advance call:

* `GET /sandbox/simulations/{simulation_id}` — returns current state and the callback delivery history.
* `POST /sandbox/simulations/{simulation_id}/advance` — unpauses with action `release`, `reject`, `approve`, etc. (valid actions vary per scenario).

# Sandbox Behavior Control Headers

Sandbox accepts a small set of HTTP request headers that override the default success path on a per-request basis. They let you exercise error branches and edge cases without contriving the upstream conditions. These headers are sandbox-only — they have no effect against `api.platform.dakota.xyz`. They are also intentionally not part of the OpenAPI spec; this section is the canonical reference.

## Available Headers

| Header                         | Type                 | Effect                                                                                                                                                                                                |
| ------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Sandbox-Error-Status`       | integer              | HTTP status code returned at the configured error step.                                                                                                                                               |
| `X-Sandbox-Error-Message`      | string (≤ 500 chars) | Error message body returned at the configured error step.                                                                                                                                             |
| `X-Sandbox-Error-Step`         | string (≤ 100 chars) | Pipeline step at which the injected error fires. Valid values: `transaction_processing`, `compliance_check`, `account_validation`, `provider_call`, `kyb_submission`, `kyb_approval`, `network_call`. |
| `X-Sandbox-Scenario`           | string enum          | Applies a preset failure mode. See the Scenarios table below.                                                                                                                                         |
| `X-Sandbox-Instant-Completion` | boolean              | When `true`, async simulation flows complete immediately rather than requiring a manual advance call. Default `false`.                                                                                |
| `X-Sandbox-Skip-Auto-Approval` | boolean              | When `true`, suppresses the 5-second KYB auto-approval. Use to test pending KYB states. Default `false`.                                                                                              |

`X-Sandbox-Error-Status`, `X-Sandbox-Error-Message`, and `X-Sandbox-Error-Step` work together — set all three to inject a custom error at a specific pipeline step. `X-Sandbox-Scenario` is the higher-level alternative: pick a named scenario and the platform fills in step, status, and message for you.

```bash theme={null}
# Apply a preset scenario and skip the manual advance step
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "X-Sandbox-Scenario: insufficient_funds" \
  -H "X-Sandbox-Instant-Completion: true" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
```

```bash theme={null}
# Inject a bespoke 503 at the provider_call step
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "X-Sandbox-Error-Status: 503" \
  -H "X-Sandbox-Error-Message: Provider degraded" \
  -H "X-Sandbox-Error-Step: provider_call" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
```

## Scenarios

Valid values for the `X-Sandbox-Scenario` header. The same list is also available programmatically via `GET /sandbox/scenarios`.

| Scenario               | Behavior                                                                                                          |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `happy_path`           | Everything succeeds immediately.                                                                                  |
| `delayed_settlement`   | Succeeds but pauses for manual progression via `POST /sandbox/simulations/{id}/advance`.                          |
| `insufficient_funds`   | Returns `400` at `transaction_processing` with "Source account has insufficient funds for this transaction".      |
| `compliance_block`     | Returns `403` at `compliance_check` with "Transaction blocked: compliance review required. Contact support."      |
| `invalid_account`      | Returns `400` at `account_validation` with "Invalid destination account or address".                              |
| `provider_maintenance` | Returns `503` at `provider_call` with "Provider temporarily unavailable for maintenance. Please try again later." |
| `network_congestion`   | Returns `429` at `transaction_processing` with "Network congested, please retry later".                           |
| `kyb_manual_review`    | KYB stays pending at `kyb_approval`; no auto-approval fires.                                                      |
| `kyb_rejected`         | Returns `403` at `kyb_submission` with "KYB verification failed: Unable to verify business information".          |
| `kyb_expired`          | Returns `400` at `kyb_submission` with "KYB session expired. Please start a new verification."                    |
| `network_timeout`      | Returns `504` at `network_call` with "Request timed out. Please try again."                                       |
| `intermittent_errors`  | Returns `500` at `provider_call` roughly 30% of the time with "Temporary error occurred. Please retry."           |
| `account_frozen`       | Returns `403` at `provider_call` with "Account is frozen pending review".                                         |
| `document_expired`     | Returns `400` at `kyb_submission` with "Document verification expired, please resubmit".                          |
| `invalid_swift`        | Returns `400` at `account_validation` with "Invalid SWIFT code provided".                                         |

# TRM Compliance Mock Patterns

Sandbox runs a mock TRM Labs compliance client that returns deterministic risk classifications based on patterns in the address being screened. This lets you exercise high-risk and manual-review code paths without sourcing real flagged wallets.

| Address pattern                         | Classification                                                                                                             |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Contains `hack` (case-insensitive)      | `RiskLevelHigh`, score `75`, categories `[KNOWN_SCAMMER, BLACKLISTED_ADDRESS]`, `is_blocked: false`, `should_alert: true`. |
| Contains `scam` (case-insensitive)      | Same as `hack`.                                                                                                            |
| EVM address starting `0xBAD0000…`       | Same as `hack`.                                                                                                            |
| EVM address starting `0xSCAM0000…`      | Same as `hack`.                                                                                                            |
| Bitcoin address starting `bc1qBADBADB…` | Same as `hack`.                                                                                                            |
| Contains `review` (case-insensitive)    | `RiskLevelMedium`, score `50`, categories `[UNUSUAL_ACTIVITY, REQUIRES_REVIEW]`, not blocked, not alerted.                 |
| Contains `manual` (case-insensitive)    | Same as `review`.                                                                                                          |
| Exact address `0xMANUAL000000000`       | Same as `review`.                                                                                                          |
| Anything else                           | `RiskLevelLow`, score `10`, no categories, not blocked, not alerted.                                                       |

```bash theme={null}
# Create a destination using a high-risk-classified address
curl -X POST https://api.platform.sandbox.dakota.xyz/recipients/$RECIPIENT_ID/destinations \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "ethereum-sepolia",
    "address": "0xBAD0000000000000000000000000000000000000"
  }'
```

The `compliance_block` scenario (set via `X-Sandbox-Scenario`) overrides the address-pattern logic and forces a `RiskLevelCritical` blocked response on every screening call — useful when you want to test the blocked-transaction branch without crafting a specific address.

# Webhook Testing

Sandbox webhooks fire from the same delivery pipeline as production — same envelope, same signing key derivation, same retry behaviour. Use `X-Dakota-Event-ID` for idempotency and verify signatures the same way you will in production. See [Webhooks](/documentation/webhooks) for the full reference.

## ngrok for Local Development

```bash theme={null}
# Expose your local server
ngrok http 3000

# Register the HTTPS tunnel as a webhook target
curl -X POST https://api.platform.sandbox.dakota.xyz/webhooks/targets \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://abc123.ngrok.io/webhooks", "active": true}'
```

## Webhook.site for Quick Testing

1. Visit `https://webhook.site` and copy the unique URL.
2. Register it as a webhook target.
3. Trigger a simulation; observe the live deliveries.

# Required Headers

<Warning>
  **`X-Idempotency-Key` is required for ALL POST endpoints.**

  Per the OpenAPI spec, every POST request must include an `X-Idempotency-Key` header — a unique UUID per logical request. Requests without it fail with `400`. Repeating the same key within the dedupe window returns the original response.

  ```bash theme={null}
  curl -X POST https://api.platform.sandbox.dakota.xyz/customers \
    -H "X-API-Key: $DAKOTA_API_KEY" \
    -H "X-Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{...}'
  ```
</Warning>

# Rate Limit Testing

Sandbox enforces the same rate limits as production. Send a burst of read requests to verify your client's backoff:

```bash theme={null}
for i in {1..150}; do
  curl -s -o /dev/null -w "%{http_code}\n" \
    https://api.platform.sandbox.dakota.xyz/customers \
    -H "X-API-Key: $DAKOTA_API_KEY" &
done
wait
```

Expect `429` responses once the per-key limit is hit.

# Common Issues

## HTTP 400 on object create with a `*-mainnet` network

The sandbox network restriction rejects mainnet network IDs. Use the testnet equivalent (`ethereum-sepolia`, `base-sepolia`, etc.) or `evm` for chain-agnostic destinations.

## HTTP 400 on a one-off transaction or simulate inbound with `amount > 2.00`

The sandbox amount cap rejects per-request amounts above \$2 USD. Cap your test fixtures at `"1.00"` or `"2.00"`.

## HTTP 400 on POST requests in general

`X-Idempotency-Key` is required on every POST request. Generate a fresh UUID per logical request.

## Webhooks not received

Confirm your endpoint is publicly reachable, returns `2xx` within 30 seconds, and has been registered with `POST /webhooks/targets`. ngrok or webhook.site are the easiest local test loops.

## Transactions stuck in processing

Use `POST /sandbox/simulate/inbound` to push the lifecycle forward. For paused stateful scenarios, use `POST /sandbox/simulations/{id}/advance`.

# Migration to Production

When you're ready to switch:

1. **Update configuration** — change the base URL to `https://api.platform.dakota.xyz` and replace sandbox API keys with production keys. Update webhook URLs to your production receiver.

2. **Environment variables**

   ```bash theme={null}
   DAKOTA_API_KEY=your_production_api_key
   DAKOTA_BASE_URL=https://api.platform.dakota.xyz
   DAKOTA_PUBLIC_KEY=your_production_webhook_public_key
   ```

3. **Switch network IDs** — replace testnet IDs (`*-sepolia`, `*-amoy`, `*-devnet`) with their mainnet counterparts. Production rejects testnet IDs.

4. **Remove cap-imposed amount limits** — the \$2 cap is sandbox-only. Use real transaction amounts in production.

5. **Roll out gradually** — start with low-value transactions, monitor error rates, and have a rollback plan.

# Next Steps

* [Authentication](authentication) — production security practices.
* [Customer Onboarding](customer-onboarding) — production KYB flows.
* [Common Flows](/documentation/common-flows) — onramp, offramp, swap, and wallet flows end-to-end.
* [Webhooks](webhooks) — webhook signature verification and event reference.

# API Reference

* [Simulate an Inbound Payment Event](/api-reference/sandbox/simulate-an-inbound-payment-event)
* [Simulate an Onboarding State Transition](/api-reference/sandbox/simulate-onboarding)
* [Get Simulation Status](/api-reference/sandbox/get-simulation-status)
* [Advance a Paused Simulation](/api-reference/sandbox/advance-a-paused-simulation)
* [List Available Simulation Scenarios](/api-reference/sandbox/list-available-simulation-scenarios)
