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

The Grid sandbox environment allows you to test your integration without making real payments. When you set up your account, you can configure production and sandbox API tokens. The sandbox token is specifically for testing and development purposes.
It corresponds to a separate platform instance in "sandbox" mode, which can only transact with the sandbox UMA addresses for testing.

## Overview

The sandbox environment provides:

1. A dedicated sandbox platform for testing
2. Test UMA addresses for simulating payments
3. Endpoints to simulate sending and receiving payments
4. All the same webhooks and flows as production, but with simulated funds

## Test UMA Addresses

The sandbox provides several test UMA addresses you can use to simulate different scenarios:

| UMA Address                              | Description                        |
| ---------------------------------------- | ---------------------------------- |
| `$success.usd@sandbox.uma.money`         | Always succeeds, sends USD         |
| `$success.eur@sandbox.uma.money`         | Always succeeds, sends EUR         |
| `$success.mxn@sandbox.uma.money`         | Always succeeds, sends MXN         |
| `$pending.long.usd@sandbox.uma.money`    | Simulates a long-pending payment   |
| `$fail.compliance.usd@sandbox.uma.money` | Simulates compliance check failure |

## Testing Outgoing Payments

To test sending payments from your platform, follow these steps:

```mermaid theme={null}
sequenceDiagram
    participant Client as Your Platform
    participant Grid as Grid Sandbox
    participant Test as Test UMA Address

    Note over Client, Grid: Testing Outgoing Payments
    Client->>Grid: GET /receiver/$success.usd@sandbox.uma.money
    Grid-->>Client: Supported currencies and requirements
    Client->>Grid: POST /quotes
    Grid-->>Client: Quote with payment instructions
    Client->>Grid: POST /sandbox/send
    Grid-->>Client: Payment simulated
    Grid->>Client: Webhook: OUTGOING_PAYMENT (COMPLETED)

    Note over Client, Grid: Testing Incoming Payments
    Client->>Grid: POST /sandbox/uma/receive
    Grid->>Client: Webhook: INCOMING_PAYMENT (PENDING)
    Client-->>Grid: HTTP 200 OK (approve payment)
    Grid->>Client: Webhook: INCOMING_PAYMENT (COMPLETED)
```

1. Look up a sandbox UMA address:

```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$success.usd@sandbox.uma.money" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```

2. Create a quote as normal:

```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "lookupId": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
    "source": {
      "sourceType": "REALTIME_FUNDING",
      "currency": "MXN"
    },
    "destination": {
      "destinationType": "UMA_ADDRESS",
      "umaAddress": "$success.usd@sandbox.uma.money"
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000
  }'
```

3. Instead of making a real bank transfer, use the sandbox send endpoint:

```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/send" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "UMA-Q12345-REF",
    "currencyCode": "USD",
    "currencyAmount": 10000
  }'
```

The sandbox will simulate the payment and send appropriate webhooks just like in production.

## Testing Incoming Payments

To test receiving payments to your platform's users, use the sandbox receive endpoint:

```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/uma/receive" \
  -H "Authorization: Basic $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
  }'
```

This will trigger the same webhook flow as a real incoming payment:

1. You'll receive an `INCOMING_PAYMENT` webhook with `status: "PENDING"`
2. Your platform should approve/reject the payment
3. On approval, you'll receive another webhook with `status: "COMPLETED"`

## Example Testing Flow

Here's a complete example of testing both directions of payments:

1. First, register a test user:

```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/users" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "umaAddress": "$test.user@your.domain",
    "platformUserId": "test_123",
    "userType": "INDIVIDUAL",
    "fullName": "Test User",
    "birthDate": "1990-01-01",
    "address": {
      "line1": "123 Test St",
      "city": "Testville",
      "state": "TS",
      "postalCode": "12345",
      "country": "US"
    }
  }'
```

2. Test receiving a payment:

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

3. Test sending a payment:

```bash theme={null}
# 1. Look up recipient
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$success.usd@sandbox.uma.money" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"

# 2. Create quote
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "lookupId": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
    "source": {
      "sourceType": "REALTIME_FUNDING",
      "currency": "MXN"
    },
    "destination": {
      "destinationType": "UMA_ADDRESS",
      "umaAddress": "$success.usd@sandbox.uma.money"
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000
  }'

# 3. Simulate sending payment
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/send" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "UMA-Q12345-REF",
    "currencyCode": "USD",
    "currencyAmount": 10000
  }'
```

## Testing Error Scenarios

You can test various error scenarios using the special sandbox UMA addresses:

1. Test compliance failures:

```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$fail.compliance.usd@sandbox.uma.money" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# ... create quote and attempt payment
```

2. Test long-pending payments:

```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$pending.long.usd@sandbox.uma.money" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# ... create quote and attempt payment
```

3. Non-existent UMA address:

```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$non.existent.usd@sandbox.uma.money" \
  -H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# ... should return 404 Not Found
```

Each of these will trigger appropriate error webhooks and status updates to help you test your error handling.

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

## Production vs Sandbox

Here are the key differences between production and sandbox environments:

1. **API Tokens**: Sandbox tokens only work in the sandbox environment and vice versa
2. **Bank Transfers**: In sandbox, you use `/sandbox/send` instead of real bank transfers
3. **Test UMA Addresses**: Special sandbox addresses for testing different scenarios
4. **Money**: No real money is moved in sandbox

Always test thoroughly in sandbox before moving to production!
