Use the sandbox to test every error path in your integration before touching real money. This is how senior payment engineers build reliability.
| Feature | Sandbox | Production |
|---|---|---|
| Real money movement | Never — simulated only | Yes — real funds |
| API keys prefix | sk_test_ / pk_test_ | sk_live_ / pk_live_ |
| Settlement timing | Configurable (instant / T+1) | NACHA schedule (T+1 batch) |
| Return code triggers | Via special account numbers | Actual RDFI responses |
| Data isolation | Completely separate | Production data |
Create a bank account with these specific account numbers to trigger the corresponding ACH return code during transfer processing.
| Account Number | Return Code | Reason | Common Cause |
|---|---|---|---|
000000000001 | R01 | Insufficient Funds | Customer has less money than transfer amount |
000000000002 | R02 | Bank Account Closed | Account exists but has been closed |
000000000003 | R03 | No Account/Cannot Locate | Routing+account number mismatch |
000000000004 | R04 | Invalid Account Number | Account number fails check digit validation |
000000000007 | R07 | Authorization Revoked | Customer canceled their debit authorization |
000000000008 | R08 | Payment Stopped | Customer placed a stop payment at their bank |
000000000010 | R10 | Customer Advises Not Authorized | Customer disputes the debit entirely |
000000000016 | R16 | Account Frozen | Account frozen due to legal order or bank decision |
000000000029 | R29 | Corporate Customer Advises Not Authorized | Business disputes the CCD debit |
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.
// 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