Host-to-Host

Test it in sandbox

Built-in test rails that return production-shaped payment data, and how to force every outcome.

Nothing to configure. A fresh sandbox shop has no payment methods wired yet, so these rails are surfaced by GET /api/v1/payment-methods from the very first call — you can charge against them out of the box. They only ever appear on a sk_test_ key; a production charge can never reach a test id.

Test rails

Charge them exactly like a real rail: pass the paymentMethodId to POST /api/v1/payments. Every charge lands pending with real payment data, and you decide the outcome.

paymentMethodIdRailCountryCurrency
sbx_speiSPEI bank transferMexicoMXN
sbx_cashOXXO Pay (cash voucher)MexicoMXN
sbx_card_mxLocal credit cardMexicoMXN
sbx_debit_mxLocal debit cardMexicoMXN
sbx_pixPIXBrazilBRL
sbx_psePSE bank transferColombiaCOP
sbx_cardCardUnited StatesUSD

What each rail returns

The payment data comes back in paymentData on the charge response and on GET /payments/{id}. The field names are the SAME ones production uses, so the branch you write here is the branch that runs live.

Deterministic — and different per order. The same merchantOrderId always produces the same CLABE, reference and barcode, so you can re-run a test and diff it byte for byte. Two different orders get different data — because that is what production does, and “did I store the CLABE against the right order?” is exactly the bug this catches.
json
// sbx_spei — bank transfer
"paymentData": {
  "method": "spei",
  "clabe": "646180283352577324",   // 18 digits, VALID check digit, one per charge
  "bankName": "STP",
  "beneficiaryName": "Key2Pay Sandbox",
  "reference": "SBX1XBET0001",
  "amountLocal": 1820.50, "currencyLocal": "MXN",
  "dueDate": "2026-08-27T21:00:00.000Z"
}

// sbx_cash — OXXO voucher
"paymentData": {
  "method": "oxxo",
  "reference": "SBX1XBET0001",
  "barcode": "93022833525773",     // what the cashier scans
  "digitableLine": "93022833525773",
  "amountLocal": 1820.50, "currencyLocal": "MXN",
  "dueDate": "2026-08-27T21:00:00.000Z"
}

// sbx_card_mx / sbx_debit_mx — local card
"paymentData": {
  "method": "card",
  "reference": "SBX1XBET0001",
  "amountLocal": 1820.50, "currencyLocal": "MXN",
  "completeAt": "hosted_checkout"  // send the buyer to checkoutUrl
}

// sbx_pix — PIX
"paymentData": {
  "method": "pix",
  "reference": "SBX1XBET0001",
  "qrCode": "00020126400014br.gov.bcb.pix...6304A1B2",   // EMV, real CRC16
  "copyPaste": "00020126400014br.gov.bcb.pix...6304A1B2",
  "amountLocal": 320.00, "currencyLocal": "BRL",
  "dueDate": "2026-08-27T21:00:00.000Z"
}
Card is the one rail you don't complete server-side. We do not tokenize PANs or run 3-D Secure on your behalf — that is what keeps your servers out of PCI scope. So the card rails return completeAt: "hosted_checkout" and the charge response carries a checkoutUrl: send the buyer there and wait for the webhook. Sandbox behaves exactly like production here — better to find that out now than in certification.

Forcing an outcome

A sandbox charge stays pending until you say otherwise. Two ways, same engine behind both — and both fire the REAL signed webhook, so this is how you test your handler.

The simulate endpoint

bash
curl -X POST https://sandbox.key2pay.ai/api/v1/api/v1/payments/TXN-.../simulate   -H "Authorization: Bearer sk_test_..."   -H "Content-Type: application/json"   -d '{"action":"paid"}'
actionstatus becomeswebhook you receive
paidcompletedpayment.completed
failedfailedpayment.failed
expiredexpiredpayment.expired
refundedrefundedpayment.refunded
chargebackchargebackchargeback.created

Or decide it at charge time

Pass Sandbox-Simulate: paid on the charge itself when you want a one-shot test — useful in CI, where a second round-trip is just noise.

bash
curl -X POST https://sandbox.key2pay.ai/api/v1/api/v1/payments   -H "Authorization: Bearer sk_test_..."   -H "Sandbox-Simulate: paid"   -H "Idempotency-Key: ord_1001"   -H "Content-Type: application/json"   -d '{"amount":100,"paymentMethodId":"sbx_spei","merchantOrderId":"ord_1001"}'

Sandbox vs production

Same endpoints, same field names, same signed webhooks, same error codes. Three things differ, and all three are the point of a sandbox:

  • No money moves. The rails are simulated; nothing reaches a bank and no settlement is owed.
  • You decide the outcome. In production the provider does, and it can take minutes (SPEI) or days (cash).
  • Fees are zero. Your real pricing applies only in production, so amounts here stay clean round numbers.
Your signature check is real from day one. Sandbox webhooks are signed with your subscription secret using the same HMAC scheme as production. If your verification passes here it passes live — and if it fails here you have found a real bug, not a sandbox quirk.