> ## Documentation Index
> Fetch the complete documentation index at: https://ramps-docs-sync-20260519.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox Testing

> Use Grid sandbox magic values to trigger specific API responses

The Grid sandbox environment simulates real payment flows without moving real money. You can control test outcomes using special account number patterns and test addresses.

## KYC/KYB verification

In sandbox, you can trigger specific KYC/KYB verification outcomes using magic suffixes in customer and beneficial owner fields. These let you test different verification flows without waiting for real review.

### Business customer verification

The **last 3 digits** of the `registrationNumber` in `businessInfo` determine the KYB status outcome when you call `POST /verifications`:

| Suffix        | `kybStatus` | Behavior                         |
| ------------- | ----------- | -------------------------------- |
| **001**       | `PENDING`   | KYB verification remains pending |
| **002**       | `REJECTED`  | KYB verification is rejected     |
| **Any other** | `APPROVED`  | KYB verification is approved     |

### Beneficial owner KYC

The **last 3 characters** of the `lastName` in `personalInfo` determine the individual KYC status outcome:

| Suffix        | `kycStatus` | Behavior                         |
| ------------- | ----------- | -------------------------------- |
| **001**       | `PENDING`   | KYC verification remains pending |
| **002**       | `REJECTED`  | KYC verification is rejected     |
| **Any other** | `APPROVED`  | KYC verification is approved     |

## Adding external accounts

The flows for creating external accounts in sandbox are the same as in production. The **last 3 digits** of an external account's primary identifier (account number, IBAN, CLABE, Spark wallet address, etc.) determine the test scenario when that account is used in transfers or quotes. For identifiers with a domain part (e.g. PIX email keys), append the test digits to the username portion — for example, `testuser.002@pix.com.br`.

### Beneficiary name verification

For account types that support beneficiary name verification, you can simulate different verification outcomes in sandbox. Use account identifiers with a `1xx` suffix to trigger verification scenarios (this range is reserved for verification and does not conflict with transfer or quote test patterns):

| Suffix        | `beneficiaryVerificationStatus` | Behavior                                                             |
| ------------- | ------------------------------- | -------------------------------------------------------------------- |
| **102**       | `NOT_MATCHED`                   | Account is valid but name does not match                             |
| **103**       | `PARTIAL_MATCH`                 | Account is valid, name is a fuzzy match                              |
| **104**       | `PENDING`                       | Verification still in progress                                       |
| **105**       | *(error)*                       | Returns `400` — invalid account                                      |
| **106**       | `UNSUPPORTED`                   | Payment rail does not support name verification                      |
| **107**       | `CHECKED_BY_RECEIVING_FI`       | Verification deferred to receiving financial institution (e.g., ACH) |
| **Any other** | `MATCHED`                       | Account is valid, name matches exactly                               |

## Transfer in

In production, internal accounts are funded by sending a bank transfer to the account's payment instructions or by pulling from an external account. In sandbox, you have two options:

### Transfer in from an external account

Use the `/transfer-in` endpoint to pull funds from an external account into an internal account. The external account's number suffix determines the outcome:

| Suffix        | Behavior                                                  |
| ------------- | --------------------------------------------------------- |
| **002**       | Insufficient funds — transfer fails immediately           |
| **003**       | Account closed/invalid — transfer fails immediately       |
| **004**       | Transfer rejected — bank rejects the transfer             |
| **005**       | Timeout/delayed failure — stays pending \~30s, then fails |
| **Any other** | Success — transfer completes normally                     |

### Sandbox fund endpoint

Instantly add funds to any internal account using `/sandbox/internal-accounts/{accountId}/fund`:

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/internal-accounts/{accountId}/fund \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 100000 }'
```

## Creating quotes (cross-currency transfers)

When creating a quote with an external account destination, the account number suffix determines the payment outcome after quote execution:

| Suffix        | Behavior                                                               |
| ------------- | ---------------------------------------------------------------------- |
| **002**       | Quote execution failed                                                 |
| **003**       | Long payment — completes after approximately 6 minutes                 |
| **004**       | Counterparty delivery failed                                           |
| **005**       | Receiving bank returned payment (completes then transitions to failed) |
| **006**       | User cancellation                                                      |
| **007**       | Payout and refund failed                                               |
| **Any other** | Successful payment                                                     |

### Executing a quote

After creating a quote, you need to fund it to trigger execution. There are two ways to do this in sandbox:

**Prefunded internal account** — If your quote's source is an internal account, fund the account using one of the methods described in [transfer in](#transfer-in), then call the quote execute endpoint to trigger the transaction:

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```

**Real-time funding via sandbox send** — If your quote uses real-time funding, the quote response includes payment instructions for you to transfer funds to. Use `/sandbox/send` to simulate this payment:

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/send \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
    "currencyCode": "USD"
  }'
```

## Transferring out funds

Use the `/transfer-out` endpoint to push funds from an internal account to an external account in the same currency. The external account's number suffix controls the outcome using the same patterns as [transfer in](#transfer-in-from-an-external-account).

## Sending to a UMA address

For UMA-based payments, use these sandbox addresses to simulate different scenarios:

| UMA Address                              | Behavior                             |
| ---------------------------------------- | ------------------------------------ |
| `$success.usd@sandbox.uma.money`         | Payment succeeds (USD)               |
| `$success.eur@sandbox.uma.money`         | Payment succeeds (EUR)               |
| `$success.mxn@sandbox.uma.money`         | Payment succeeds (MXN)               |
| `$pending.long.usd@sandbox.uma.money`    | Simulates a long-pending payment     |
| `$fail.compliance.usd@sandbox.uma.money` | Simulates a compliance check failure |

### Simulating incoming UMA payments

Use the sandbox receive endpoint to simulate an incoming UMA payment to one of your platform's users:

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/uma/receive \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "senderUmaAddress": "$success.usd@sandbox.uma.money",
    "receiverUmaAddress": "$your.user@your.domain",
    "receivingCurrencyCode": "USD",
    "receivingCurrencyAmount": 5000
  }'
```

## Global Account magic values

The Grid sandbox accepts a small set of magic values that bypass real auth and credential checks for Global Account flows, so you can exercise the full request shape without standing up Turnkey, WebAuthn, or an OIDC provider. These values are sandbox-only — production enforces real signature verification, WebAuthn assertion, and OIDC nonce binding.

A wrong magic value (or any other value) returns `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed.

### Email OTP code

Pass `000000` as the body `otp` on `POST /auth/credentials/{id}/verify` when the credential type is `EMAIL_OTP`. The sandbox skips OTP delivery and accepts this value as a valid response to the issued challenge.

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
  -d '{
    "type": "EMAIL_OTP",
    "otp": "000000",
    "clientPublicKey": "04f45f2a..."
  }'
```

Any other code returns `401 UNAUTHORIZED` with `reason: "Invalid OTP code"`.

### Passkey assertion signature

Pass `sandbox-valid-passkey-signature` as `assertion.signature` on `POST /auth/credentials/{id}/verify` when the credential type is `PASSKEY`. The sandbox accepts the rest of the assertion as-is and skips the WebAuthn signature check.

Passkey reauthentication is a two-step `/challenge` → `/verify` flow. The `clientPublicKey` is sent on `/challenge` (so Grid can seal the session signing key to your device) — the magic value bypasses the credential check, not the HPKE plumbing, so the public key is still required.

```bash theme={null}
# 1. /challenge with clientPublicKey
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/challenge \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "clientPublicKey": "04f45f2a..."
  }'

# 2. /verify with the magic signature, no clientPublicKey
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
  -d '{
    "type": "PASSKEY",
    "assertion": {
      "credentialId": "...",
      "clientDataJson": "...",
      "authenticatorData": "...",
      "signature": "sandbox-valid-passkey-signature"
    }
  }'
```

Any other signature returns `401 UNAUTHORIZED` with `reason: "Invalid passkey signature"`.

### OAuth (OIDC) token

Pass `sandbox-valid-oidc-token` as the body `oidcToken` on both `POST /auth/credentials` (OAUTH create) and `POST /auth/credentials/{id}/verify` (OAUTH).

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
  -d '{
    "type": "OAUTH",
    "oidcToken": "sandbox-valid-oidc-token",
    "clientPublicKey": "04f45f2a..."
  }'
```

Any other token returns `401 UNAUTHORIZED` with `reason: "Invalid OIDC token"`.

<Note>
  **OAUTH create still requires a JWT-shaped token.** On the initial `POST /auth/credentials` (OAUTH create), the `oidcToken` must be a structurally valid JWT (`header.payload.signature`) so Grid can decode the `iss` claim and resolve the provider name. The literal `sandbox-valid-oidc-token` works on `verify` but not on `create` — for `create`, sign your own dummy JWT with any payload that includes a recognized `iss` claim. The sandbox bypasses signature verification, not JWT structure parsing.
</Note>

### Wallet signature header

Pass `sandbox-valid-signature` as the `Grid-Wallet-Signature` HTTP header on any signed-retry flow:

* `POST /auth/credentials` (add-additional-credential signed retry)
* `DELETE /auth/credentials/{id}` (revoke credential)
* `DELETE /auth/sessions/{id}` (revoke session)
* `POST /internal-accounts/{id}/export` (export wallet)
* `PATCH /internal-accounts/{id}` (update wallet privacy)
* `POST /quotes/{quoteId}/execute` (when source is an embedded wallet)

```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/Quote:abc123/execute \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
  -H "Grid-Wallet-Signature: sandbox-valid-signature"
```

Any other header value returns `401 UNAUTHORIZED` with `reason: "Invalid Grid-Wallet-Signature"`.
