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

# Sandbox Testing

> Test ramp flows safely without moving real funds

The Grid Sandbox environment provides a complete testing environment for ramp operations, allowing you to validate on-ramp and off-ramp flows without using real money or cryptocurrency.

## Sandbox overview

Sandbox mirrors production behavior while using simulated funds:

* **Same API endpoints**: Use identical API calls as production
* **Simulated funding**: Mock bank transfers and crypto deposits
* **Real webhooks**: Receive actual webhook notifications
* **No real money**: All transactions use test funds
* **Isolated environment**: Sandbox data never affects production

<Info>
  Sandbox is perfect for development, testing, and demonstrating ramp
  functionality before going live.
</Info>

## Getting started

### Create sandbox credentials

1. Log into the Grid dashboard
2. Navigate to **Settings** → **API Keys**
3. Click **Create API Key** and select **Sandbox** environment
4. Save your API key ID and secret securely

<Warning>
  Sandbox credentials only work with the sandbox environment. They cannot access
  production data or move real funds.
</Warning>

### Configure sandbox webhook

Set up a webhook endpoint for sandbox notifications:

```bash theme={null}
curl -X PATCH 'https://api.lightspark.com/grid/2025-10-13/config' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "webhookEndpoint": "https://api.yourapp.dev/webhooks/grid"
  }'
```

<Tip>
  Use tools like ngrok to expose local webhook endpoints during development:
  `ngrok http 3000`
</Tip>

## Testing on-ramps (Fiat → Crypto)

Simulate the complete on-ramp flow in sandbox:

### Step 1: Create a test customer

```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "platformCustomerId": "test_user_001",
    "customerType": "INDIVIDUAL",
    "fullName": "Alice Test",
    "email": "alice@example.com",
    "birthDate": "1990-01-15",
    "address": {
      "line1": "123 Test Street",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94105",
      "country": "US"
    }
  }'
```

<Check>In sandbox, customers are automatically approved for testing.</Check>

### Step 2: Create an external account for the destination wallet

```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "Customer:sandbox001",
    "currency": "BTC",
    "accountInfo": {
      "accountType": "SPARK_WALLET",
      "address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
    }
  }'
```

### Step 3: Create an on-ramp quote (just-in-time funding)

```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "customerId": "Customer:sandbox001",
      "currency": "USD"
    },
    "destination": {
      "destinationType": "ACCOUNT",
      "accountId": "ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456"
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000,
    "description": "Test on-ramp conversion"
  }'
```

The quote response includes payment instructions with a reference code.

### Step 4: Simulate funding

Use the sandbox endpoint to simulate receiving the fiat 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 '{
    "reference": "RAMP-ABC123",
    "currencyCode": "USD",
    "currencyAmount": 10000
  }'
```

<Note>
  The reference code must match the one provided in the quote's payment
  instructions.
</Note>

### Step 5: Verify completion

Within seconds, you'll receive a webhook notification confirming the on-ramp completed:

```json theme={null}
{
  "transaction": {
    "id": "Transaction:sandbox025",
    "status": "COMPLETED",
    "type": "OUTGOING",
    "sentAmount": {
      "amount": 10000,
      "currency": { "code": "USD" }
    },
    "receivedAmount": {
      "amount": 95000,
      "currency": { "code": "BTC" }
    },
    "settledAt": "2025-10-03T15:02:30Z"
  },
  "type": "OUTGOING_PAYMENT"
}
```

## Testing off-ramps (Crypto → Fiat)

Simulate the complete off-ramp flow:

### Step 1: Fund internal account with crypto

Simulate a Bitcoin deposit to the customer's internal account using the sandbox funding endpoint:

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

Replace `InternalAccount:btc001` with your actual BTC internal account ID.

<Check>
  You'll receive an `ACCOUNT_STATUS` webhook showing the updated balance.
</Check>

### Step 2: Create external bank account

```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "Customer:sandbox001",
    "currency": "USD",
    "platformAccountId": "test_bank_001",
    "accountInfo": {
      "accountType": "US_ACCOUNT",
      "accountNumber": "123456001",
      "routingNumber": "021000021",
      "accountCategory": "CHECKING",
      "bankName": "Test Bank",
      "beneficiary": {
        "beneficiaryType": "INDIVIDUAL",
        "fullName": "Alice Test",
        "birthDate": "1990-01-15",
        "nationality": "US",
        "address": {
          "line1": "123 Test Street",
          "city": "San Francisco",
          "state": "CA",
          "postalCode": "94105",
          "country": "US"
        }
      }
    }
  }'
```

<Note>
  In sandbox, you can use special account number patterns to test different scenarios. The **last 3 digits** determine the behavior: **002** (insufficient funds), **003** (account closed), **004** (transfer rejected), **005** (timeout/delayed failure). Any other ending succeeds normally. See "Testing transfer failures" below for details.
</Note>

### Step 3: Create and execute off-ramp quote

```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "accountId": "InternalAccount:sandbox_btc001"
    },
    "destination": {
      "accountId": "ExternalAccount:sandbox_bank001",
      "currency": "USD"
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 5000000,
    "description": "Test off-ramp conversion"
  }'
```

Then execute the quote:

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

<Info>
  In sandbox, off-ramp conversions complete instantly. In production, bank
  settlement may take 1-3 business days.
</Info>

## Testing transfer failures

### External account test patterns

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

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

## 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) |
| **109**       | *(error)*                       | Returns `500` — simulated API error                                  |
| **Any other** | `MATCHED`                       | Account is valid, name matches exactly                               |

## Test scenarios

### Successful conversions

The complete on-ramp and off-ramp flows described in the sections above demonstrate successful conversion scenarios. For quick reference:

**On-ramp test (USD → BTC):**

1. Create customer and quote with payment instructions
2. Use `/sandbox/send` to simulate funding
3. Verify completion via webhook

**Off-ramp test (BTC → USD):**

1. Fund BTC internal account with `/sandbox/internal-accounts/{accountId}/fund`
2. Create external bank account (use default account number for success)
3. Create and execute quote
4. Verify completion via webhook

### Failed conversions

Test error scenarios systematically using the magic account patterns:

**1. Test external account insufficient funds (002):**

```bash theme={null}
# Create account with insufficient funds pattern
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "Customer:sandbox001",
    "currency": "USD",
    "accountInfo": {
      "accountType": "US_ACCOUNT",
      "accountNumber": "000000002",
      "routingNumber": "021000021",
      "accountCategory": "CHECKING",
      "beneficiary": {
        "beneficiaryType": "INDIVIDUAL",
        "fullName": "Test User"
      }
    }
  }'

# Attempt off-ramp to this account - will fail immediately
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# Response: 400 Bad Request with insufficient funds error
```

**2. Test account closed (003):**

```bash theme={null}
# Create account with closed pattern
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -d '{"accountNumber": "000000003", ...}'

# Attempt to use - will fail with account closed error
```

**3. Test insufficient balance in internal account:**

```bash theme={null}
# Create quote from empty internal account
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "accountId": "InternalAccount:empty_btc"
    },
    "destination": {
      "accountId": "ExternalAccount:bank001",
      "currency": "USD"
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000000
  }'

# Execute will fail with insufficient balance error
```

**4. Test invalid wallet address:**

```bash theme={null}
# Attempt to create an external account with invalid Spark address
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "currency": "BTC",
    "accountInfo": {
      "accountType": "SPARK_WALLET",
      "address": "invalid_address"
    }
  }'
# Response: 400 Bad Request with validation error
```

## Moving to Production

When you're ready to move to production:

1. Generate production API tokens in the dashboard
2. Swap those credentials for the sandbox credentials in your environment variables
3. Remove any sandbox-specific test patterns from your code
4. Configure production webhook endpoints
5. Test with small amounts first

## Next steps

* [Webhooks](/ramps/platform-tools/webhooks) - Handle real-time notifications
* [Fiat-to-Crypto Conversion](/ramps/conversion-flows/fiat-crypto-conversion) - Implement production flows
* [Self-Custody Wallets](/ramps/conversion-flows/self-custody-wallets) - Advanced wallet integration
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Configure production settings
* [API Reference](/api-reference) - Complete API documentation
