Sandbox
Sandbox-Simulate header
Drive deterministic outcomes (paid / failed / expired / chargeback / slow_payment) from CI via a single request header. No UI needed.
The playground at /sandbox/login is great for hands-on debugging, but CI / automated tests need a programmatic way to drive outcomes. Pass the Sandbox-Simulate header on POST /api/v1/payments and we schedule the transition for you — the same state machine the playground uses, no clicks required.
The header is honoured ONLY in sandbox. In production it's silently ignored — a misconfigured test suite can't accidentally drive live data.
Accepted values
| Value | Delay | Outcome | Webhooks fired |
|---|---|---|---|
| paid | 5 s | pending → completed | payment.completed |
| failed | 5 s | pending → failed | payment.failed |
| expired | 30 s | pending → expired | payment.failed |
| chargeback | 30 s + 5 s | pending → completed → chargeback | payment.completed, chargeback.created |
| slow_payment | 5 min | pending → completed (very late) | payment.completed |
Usage
bash
# Create a payment that auto-pays in 5 seconds:
curl https://sandbox.key2pays.com/api/v1/payments \
-H "Authorization: Bearer sk_test_51N8mP...exampleK3Y" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-H "Sandbox-Simulate: paid" \
-d '{ "amount": 50, "paymentMethodId": "1008", "country": "MEX" }'
# Create a payment that auto-fails in 5 seconds:
curl https://sandbox.key2pays.com/api/v1/payments \
-H "Authorization: Bearer sk_test_51N8mP...exampleK3Y" \
-H "Sandbox-Simulate: failed" \
-d '{ … }'
# Create a payment that completes then disputes — perfect for testing
# your chargeback.created webhook handler:
curl https://sandbox.key2pays.com/api/v1/payments \
-H "Authorization: Bearer sk_test_51N8mP...exampleK3Y" \
-H "Sandbox-Simulate: chargeback" \
-d '{ … }'Typical CI pattern
javascript
// Pattern: create tx with simulate, poll until terminal status, assert.
async function testHappyPath() {
// Spin up a local webhook receiver before this test (e.g. ngrok),
// register it once during test setup, share across tests.
const created = await api.post("/payments", {
headers: { "Sandbox-Simulate": "paid" },
body: { amount: 50, paymentMethodId: "1008", country: "MEX" },
});
expect(created.status).toBe("pending");
// Wait for the webhook (up to 10s — the simulate fires at 5s):
const event = await webhookReceiver.next("payment.completed", 10000);
expect(event.data.id).toBe(created.transactionId);
// Confirm the tx now reads as completed:
const fetched = await api.get(`/payments/${created.transactionId}`);
expect(fetched.status).toBe("completed");
}