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

# Sandbox simulation testing for Pay by Bank

> Test charge, payout, and return scenarios with deterministic outcomes in the Straddle Pay by Bank sandbox using simulation amounts and test paykeys.

Testing payment workflows in a sandbox environment traditionally requires navigating complex setup processes and waiting for simulated payment network processing delays. Our sandbox simulation feature transforms this experience by allowing you to define specific outcomes for your test scenarios, making integration testing both faster and more reliable.

<Info>
  This guide walks you through using sandbox simulations to test various payment scenarios efficiently. You'll learn how to simulate successful payments, fraud detection, insufficient funds, and other real-world scenarios without complex manual setup or extended wait times.
</Info>

## Understanding Sandbox Simulations

In production environments, payment processing involves asynchronous workflows, network processing windows, and various unpredictable outcomes. While our sandbox maintains this asynchronous behavior to ensure realistic testing, we've introduced a powerful simulation feature that gives you control over test transaction outcomes.

The core of this system is the `config` object with its `sandbox_outcome` field. Think of this as your way to instruct the sandbox exactly how a test transaction should behave, while still maintaining the proper asynchronous flow that mirrors production behavior.

<Warning>
  Sandbox simulations maintain asynchronous processing to match production behavior. While outcomes are deterministic, they still follow realistic timing patterns to ensure your integration handles asynchronous workflows correctly.
</Warning>

## How Simulations Work

When you create an entity (`customer`, `paykey`, `charge`, or `payout`) in the sandbox, you can include a `config` object that specifies your desired outcome:

```json theme={null}
{
  "config": {
    "sandbox_outcome": "paid"
  }
}
```

This configuration tells the sandbox to simulate a specific scenario. For example, setting `"sandbox_outcome": "paid"` on a charge causes it to:

1. Process normally through the initial states
2. Transition to `scheduled` ->`pending` status
3. Automatically move to `paid` status within a few minutes

<Note>
  In the sandbox environment, simulated payments process every minute rather than waiting for standard network windows. This accelerates your testing cycle while maintaining realistic state transitions.
</Note>

## Available Simulation Outcomes

Understanding the available simulation options helps you create comprehensive test scenarios that cover all possible production behaviors. Let's explore what you can simulate for each entity type.

<Tabs>
  <Tab title="Customers">
    Control customer verification status during creation:

    | Outcome    | Description                              | Use Case                           |
    | ---------- | ---------------------------------------- | ---------------------------------- |
    | `standard` | Customer undergoes normal review process | Testing standard onboarding flow   |
    | `verified` | Customer automatically becomes verified  | Testing post-verification features |
    | `rejected` | Customer automatically gets rejected     | Testing rejection handling         |
    | `review`   | Customer enters manual review status     | Testing review queue workflows     |
  </Tab>

  <Tab title="Paykeys">
    Define paykey authorization states:

    | Outcome    | Description                          | Use Case                                                 |
    | ---------- | ------------------------------------ | -------------------------------------------------------- |
    | `standard` | Paykey follows normal review process | Testing standard authorization                           |
    | `active`   | Paykey becomes immediately active    | Testing active payment methods                           |
    | `rejected` | Paykey gets rejected                 | Testing rejection scenarios                              |
    | `review`   | Paykey requires manual review        | Test scenarios requiring human or automated intervention |
  </Tab>

  <Tab title="Charges & Payouts">
    Simulate various payment outcomes:

    **Success Scenarios**

    | Outcome    | Description        | Timing                             |
    | ---------- | ------------------ | ---------------------------------- |
    | `standard` | Normal processing  | Follows standard windows           |
    | `paid`     | Successful payment | Transitions to paid within minutes |

    **Hold and Cancellation Scenarios**

    | Outcome                       | Description                    | Next Steps                    |
    | ----------------------------- | ------------------------------ | ----------------------------- |
    | `on_hold_daily_limit`         | Held due to daily limits       | Can be manually released      |
    | `cancelled_for_fraud_risk`    | Cancelled for fraud detection  | Terminal state                |
    | `cancelled_for_balance_check` | Cancelled due to balance check | Terminal state (charges only) |

    **Failure Scenarios**

    | Outcome                      | Description                 | Error Code |
    | ---------------------------- | --------------------------- | ---------- |
    | `failed_insufficient_funds`  | Fails due to NSF            | R01        |
    | `failed_customer_dispute`    | Fails due to dispute        | R05        |
    | `failed_closed_bank_account` | Fails due to closed account | R02        |

    **Reversal Scenarios**

    | Outcome                        | Description                           | Error Code |
    | ------------------------------ | ------------------------------------- | ---------- |
    | `reversed_insufficient_funds`  | Paid then reversed for NSF            | R01        |
    | `reversed_customer_dispute`    | Paid then reversed for dispute        | R05        |
    | `reversed_closed_bank_account` | Paid then reversed for closed account | R02        |
  </Tab>
</Tabs>

<Tip>
  Reversal scenarios are particularly valuable for testing reconciliation logic, as they simulate payments that initially succeed but are later reversed—a common real-world scenario that can be challenging to handle correctly.
</Tip>

## Practical Examples

Let's walk through complete testing scenarios, starting from creating test entities to processing payments. These examples demonstrate how to build realistic test cases from start to finish.

### Prerequisites

Before testing payment flows, ensure you have:

* Created a sandbox organization and account
* Obtained your sandbox API credentials
* Set up [webhook](/webhooks/overview/events) endpoints for receiving notifications

### Example 1: Testing Successful Payment Flow

This example demonstrates the complete happy path for payment processing.

<Steps>
  <Step title="Create a verified customer">
    First, create a customer that's pre-verified to skip the review process:

    ```bash theme={null}
    curl --request POST \
      --header 'Authorization: Bearer YOUR_SECRET_API_KEY' \
      --url https://sandbox.straddle.com/v1/customers \
      --header 'Content-Type: application/json' \
      --data '{
      "name": "Ron Swanson",
      "type": "individual",
      "email": "user@example.com",
      "phone": "+12128675309",
      "device": {
        "ip_address": "192.168.1.1"
      },
      "config": {
         "sandbox_outcome": "verified"
      }
    }'
    ```

    Save the `customer_id` from the response—you'll need it for the next steps.
  </Step>

  <Step title="Create an active paykey">
    Create a payment method that's immediately active:

    ```bash theme={null}
    curl https://sandbox.straddle.com/v1/bridge/bank_account \
      --request POST \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
      --header 'Straddle-Account-Id: YOUR_ACCOUNT_ID' \
      --data '{
        "customer_id": "YOUR_CUSTOMER_ID",
        "account_number": "123456789",
        "routing_number": "021000021",
        "account_type": "checking",
        "config": {
          "sandbox_outcome": "active"
        }
      }'
    ```

    The response includes a `paykey` that you'll use to create charges.
  </Step>

  <Step title="Create a successful charge">
    Now create a charge that will automatically succeed:

    ```bash theme={null}
    curl https://sandbox.straddle.com/v1/charges \
      --request POST \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
      --data '{
      	"paykey": "<string>",
      	"description": "Monthly subscription fee",
      	"amount": "10000",
      	"currency": "<string>",
     	"payment_date": "2019-12-27",
      	"consent_type": "internet",
      	"device": {
        	"ip_address": "192.168.1.1"
      	},
      	"external_id": "<string>",
      	"config": {
        	"balance_check": "enabled",
    		"sandbox_outcome": "paid"
     	}
      }'
    ```

    This charge will:

    * Create with status `created`
    * Move to `pending` as it enters processing
    * Transition to `paid` within a few minutes
  </Step>

  <Step title="Monitor webhook events">
    Your webhook endpoint should receive these status events:

    1. `created` - Initial charge creation
    2. `scheduled` - Transaction verified and queued for originaton
    3. `pending` - Charge delivered to network
    4. `paid` - Funding complete
  </Step>
</Steps>

### Example 2: Testing Fraud Detection

This scenario helps you verify your system properly handles fraud prevention measures.

<Steps>
  <Step title="Create test entities">
    Use a new `customer` and `paykey` creation process from Example 1, or reuse existing test entities.
  </Step>

  <Step title="Create a charge flagged for fraud">
    ```bash theme={null}
    curl https://sandbox.straddle.com/v1/charges \
      --request POST \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
      --data '{
      	"paykey": "<string>",
      	"description": "Monthly subscription fee",
      	"amount": "10000",
      	"currency": "USD",
     	"payment_date": "2025-05-27",
      	"consent_type": "internet",
      	"device": {
        	"ip_address": "192.168.1.1"
      	},
      	"external_id": "ABCD-4567",
      	"config": {
        	"balance_check": "enabled",
    		"sandbox_outcome": "cancelled_for_fraud_risk"
     	}
      }'
    ```
  </Step>

  <Step title="Handle the cancellation">
    The charge will be immediately cancelled. Your system should:

    * Receive a `cancelled` webhook status
    * Display appropriate messaging to users
    * Log the fraud detection for analysis
  </Step>
</Steps>

### Example 3: Testing Payment Reversals

Reversals represent a particularly challenging scenario since the payment initially succeeds. This example shows how to test your reversal handling logic.

<Steps>
  <Step title="Create a customer with review status">
    For this test, let's create a customer that requires review:

    ```bash theme={null}
    curl --request POST \
      --header 'Authorization: Bearer YOUR_SECRET_API_KEY' \
      --url https://sandbox.straddle.com/v1/customers \
      --header 'Content-Type: application/json' \
      --data '{
      "name": "Ron Swanson",
      "type": "individual",
      "email": "user2@example.com",
      "phone": "+12128675308",
      "device": {
        "ip_address": "192.168.1.1"
      },
      "config": {
         "sandbox_outcome": "verified"
      }
    }'
    ```
  </Step>

  <Step title="Create a paykey">
    ```bash theme={null}
    curl https://sandbox.straddle.com/v1/paykeys \
      --request POST \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
      --header 'Straddle-Account-Id: YOUR_ACCOUNT_ID' \
      --data '{
        "customer_id": "YOUR_CUSTOMER_ID",
        "type": "bank_account",
        "bank_account": {
          "account_number": "987654321",
          "routing_number": "021000021",
          "account_type": "checking"
        },
        "config": {
          "sandbox_outcome": "active"
        }
      }'
    ```
  </Step>

  <Step title="Create a charge that will reverse">
    ```bash theme={null}
    curl https://sandbox.straddle.com/v1/charges \
      --request POST \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
      --data '{
      	"paykey": "<string>",
      	"description": "Monthly subscription fee",
      	"amount": "10000",
      	"currency": "USD",
     	"payment_date": "2025-05-27",
      	"consent_type": "internet",
      	"device": {
        	"ip_address": "192.168.1.1"
      	},
      	"external_id": "ABCD-1234",
      	"config": {
        	"balance_check": "enabled",
    		"sandbox_outcome": "reversed_insufficient_funds"
     	}
      }'
    ```
  </Step>

  <Step title="Track the complete lifecycle">
    This creates a realistic reversal scenario where:

    1. The charge processes normally
    2. Transitions to `paid` status
    3. Your webhook receives a success notification
    4. Minutes later, the charge reverses to `reversed` status
    5. Your webhook receives a reversal notification with code R01

    Your system should handle both the initial success and subsequent reversal appropriately.
  </Step>
</Steps>

### Example 4: Testing Daily Limit Holds

This scenario tests your ability to handle and release held payments.

```bash theme={null}
# Create a charge that exceeds daily limits
curl https://sandbox.straddle.com/v1/charges \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
  --data '{
  	"paykey": "<string>",
  	"description": "Monthly subscription fee",
  	"amount": "10000",
  	"currency": "USD",
 	"payment_date": "2025-05-27",
  	"consent_type": "internet",
  	"device": {
    	"ip_address": "192.168.1.1"
  	},
  	"external_id": "ABCD-123456789",
  	"config": {
    	"balance_check": "enabled",
		"sandbox_outcome": "on_hold_daily_limit"
 	}
  }'
```

The charge will be created with `on_hold` status. You can then test releasing the hold through your admin interface or API endpoints.

## Production Behavior

Understanding how sandbox simulations behave in production is crucial for safe deployment. The system is designed with multiple safeguards to prevent any simulation logic from affecting production operations.

<Warning>
  In production environments:

  * The `sandbox_outcome` field is nullable at the API level
  * If included, it must be set to `"standard"` to pass validation
  * The field is excluded from all API responses
  * No simulation logic runs in production systems
</Warning>

This design ensures that:

* Test code accidentally deployed to production won't cause unexpected behavior
* Production API responses remain clean and consistent
* There's no performance impact from simulation logic
* Your production integration remains secure and predictable

## Best Practices

To maximize the value of sandbox simulations, consider these comprehensive testing strategies.

### Create Comprehensive Test Suites

Build test suites that cover every possible outcome your integration might encounter:

```javascript theme={null}
// Example comprehensive test suite structure
const testScenarios = [
  // Success scenarios
  { name: "Standard processing", outcome: "standard" },
  { name: "Successful payment", outcome: "paid" },
  
  // Failure scenarios
  { name: "Insufficient funds", outcome: "failed_insufficient_funds" },
  { name: "Customer dispute", outcome: "failed_customer_dispute" },
  { name: "Closed account", outcome: "failed_closed_bank_account" },
  
  // Reversal scenarios
  { name: "NSF reversal", outcome: "reversed_insufficient_funds" },
  { name: "Dispute reversal", outcome: "reversed_customer_dispute" },
  
  // Hold and cancellation scenarios
  { name: "Daily limit hold", outcome: "on_hold_daily_limit" },
  { name: "Fraud detection", outcome: "cancelled_for_fraud_risk" },
  { name: "Balance check failure", outcome: "cancelled_for_balance_check" }
];

// Run each scenario and verify your system handles it correctly
for (const scenario of testScenarios) {
  await testPaymentScenario(scenario);
  await verifySystemState(scenario.expectedState);
  await verifyWebhookHandling(scenario.expectedEvents);
}
```

### Document Test Data

Maintain clear documentation of your test scenarios and their purposes:

```javascript theme={null}
// Good practice: Self-documenting test data
const createTestCharge = async (scenario) => {
  const charge = await straddle.charges.create({
    amount: scenario.amount || 5000,
    currency: "usd",
    customer_id: scenario.customerId,
    paykey: scenario.paykey,
    description: `Test: ${scenario.description}`,
    metadata: {
      test_scenario: scenario.name,
      expected_outcome: scenario.outcome,
      test_run_id: generateTestRunId(),
      tested_at: new Date().toISOString()
    },
    config: {
      sandbox_outcome: scenario.outcome
    }
  });
  
  // Log test execution for debugging
  console.log(`Created test charge ${charge.id} for scenario: ${scenario.name}`);
  return charge;
};
```

## Troubleshooting

When simulations don't behave as expected, check these common issues and their solutions.

<AccordionGroup>
  <Accordion title="Simulation not working as expected">
    **Symptoms:**

    * Entity doesn't follow the specified outcome
    * Unexpected status transitions
    * Missing webhook events

    **Solutions:**

    1. Verify you're using the sandbox environment URL: `https://sandbox.straddle.com`
    2. Check that the outcome value is spelled exactly as documented
    3. Ensure the `config` object is properly formatted in your JSON request
    4. Confirm your API credentials have the necessary permissions
  </Accordion>

  <Accordion title="Timing seems incorrect">
    **Symptoms:**

    * Transitions happen too quickly or slowly
    * Unexpected delays between states
    * Webhooks arrive out of order

    **Solutions:**

    1. Remember sandbox processes payments every minute (not real ACH timing)
    2. Network latency can affect webhook delivery order
    3. Use webhook event timestamps, not arrival time, for sequencing
    4. Implement idempotency to handle duplicate webhook deliveries
  </Accordion>

  <Accordion title="Webhook events missing or duplicated">
    **Symptoms:**

    * Expected webhooks don't arrive
    * Receiving duplicate webhook events
    * Webhook signature validation fails

    **Solutions:**

    1. Verify your webhook endpoint returns 200 status quickly
    2. Check webhook endpoint configuration in your account settings
    3. Ensure your endpoint can handle concurrent requests
    4. Implement proper webhook signature validation
    5. Log all received webhooks for debugging
  </Accordion>

  <Accordion title="Production code includes sandbox config">
    **Symptoms:**

    * Concerns about sandbox config in production
    * Validation errors in production
    * Unexpected behavior after deployment

    **Solutions:**

    1. The field is safely ignored in production
    2. Use environment variables to conditionally include config
    3. Set up separate test and production code paths
    4. Add production deployment checks to catch test code
  </Accordion>
</AccordionGroup>

<Info>
  While sandbox simulations make testing significantly easier, always perform end-to-end testing with standard processing before deploying to production. This ensures your integration handles the full complexity of real-world payment processing, including edge cases and timing variations that simulations might not capture.
</Info>
