Beginner 20 min

Master Sandbox Testing with Return Codes

Use the sandbox to test every error path in your integration before touching real money. This is how senior payment engineers build reliability.

1
Sandbox vs. Production — Key Differences
FeatureSandboxProduction
Real money movementNever — simulated onlyYes — real funds
API keys prefixsk_test_ / pk_test_sk_live_ / pk_live_
Settlement timingConfigurable (instant / T+1)NACHA schedule (T+1 batch)
Return code triggersVia special account numbersActual RDFI responses
Data isolationCompletely separateProduction data
2
Complete Return Code Trigger Reference

Create a bank account with these specific account numbers to trigger the corresponding ACH return code during transfer processing.

Account NumberReturn CodeReasonCommon Cause
000000000001R01Insufficient FundsCustomer has less money than transfer amount
000000000002R02Bank Account ClosedAccount exists but has been closed
000000000003R03No Account/Cannot LocateRouting+account number mismatch
000000000004R04Invalid Account NumberAccount number fails check digit validation
000000000007R07Authorization RevokedCustomer canceled their debit authorization
000000000008R08Payment StoppedCustomer placed a stop payment at their bank
000000000010R10Customer Advises Not AuthorizedCustomer disputes the debit entirely
000000000016R16Account FrozenAccount frozen due to legal order or bank decision
000000000029R29Corporate Customer Advises Not AuthorizedBusiness disputes the CCD debit
Coverage priority

At minimum, your test suite should cover R01 (retry logic), R02 (account off-board flow), R03 (account verification failure), and R10 (dispute/chargeback flow). These four codes cover ~90% of production returns.

3
Build a Return Code Test Suite
// test/return-codes.test.js
const client = require('./client');
const { expect } = require('chai');

const RETURN_CODE_TRIGGERS = {
  R01: '000000000001',
  R02: '000000000002',
  R03: '000000000003',
  R10: '000000000010'
};

for (const [code, accountNumber] of Object.entries(RETURN_CODE_TRIGGERS)) {
  it(`should handle return code ${code}`, async () => {
    const account = await client.bankAccounts.create({
      account_type: 'checking',
      routing_number: '021000021',
      account_number: accountNumber,
      owner: { name: 'Test User', email: 'test@example.com' }
    });

    const transfer = await client.transfers.create({
      amount: 1000, currency: 'USD', direction: 'debit',
      source: account.id
    });

    // In sandbox, transfer returns immediately with return_code
    expect(transfer.status).to.equal('returned');
    expect(transfer.return_code).to.equal(code);
  });
}
# test_return_codes.py
import pytest
from client import client

RETURN_CODE_TRIGGERS = {
    "R01": "000000000001",
    "R02": "000000000002",
    "R03": "000000000003",
    "R10": "000000000010",
}

@pytest.mark.parametrize("code,account_num", RETURN_CODE_TRIGGERS.items())
def test_return_code(code, account_num):
    account = client.bank_accounts.create(
        account_type="checking",
        routing_number="021000021",
        account_number=account_num,
        owner={"name": "Test", "email": "t@test.com"}
    )
    transfer = client.transfers.create(
        amount=1000, currency="USD",
        direction="debit", source=account["id"]
    )
    assert transfer["status"]      == "returned"
    assert transfer["return_code"] == code
Next Steps