The complete flow
Six steps from credentials to a settled charge, with the real request and response of each.
Six steps. Steps 1 and 2 happen once per session; 3 through 6 happen per charge.
1. Authenticate
You can send your sk_ key as the bearer on every call and skip this step entirely. Exchanging it for a token is optional and worth it at volume: the token is verified with an HMAC instead of a credential lookup.
curl -X POST https://sandbox.key2pay.ai/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"apiKey":"pk_test_…","secretKey":"sk_test_…"}'Returns an accessToken valid for 1 hour and a refreshToken valid for 30 days, plus the shop the credentials belong to.
2. Ask what you can charge
Never hardcode a method list. Coverage changes per shop, per country and per provider health, and this endpoint returns only what this shop can actually charge right now — a method that would fail routing is not in the response.
curl https://sandbox.key2pay.ai/api/v1/payment-methods?country=MX \ -H "Authorization: Bearer sk_test_…"
Each item carries a code (stable, ours), the limits in USD, and iconUrl so you can render the method with its real logo.
3. Create the charge
The amount is always in USD. We convert to the buyer's local currency and return both figures.
curl -X POST https://sandbox.key2pay.ai/api/v1/payments \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-88213-attempt-1" \
-d '{
"amount": 100,
"paymentMethodId": "1221",
"country": "MEX",
"merchantOrderId": "order-88213",
"userEmail": "buyer@example.com",
"userName": "Ana Torres",
"documentId": "TOAA900101HDF"
}'missing_required_fields. Different rails need different buyer data — Mexican SPEI and OXXO need an RFC/CURP, Brazilian PIX needs a CPF. When the winning provider needs a field you did not send, we refuse before creating anything and tell you exactly which field, with its type and a human label:{ "error": {
"code": "missing_required_fields",
"details": { "missingFields": [
{ "key": "documentId", "type": "document", "label": "RFC o CURP" }
] } } }4. Render the payment data
Direct rails return the instrument in paymentData; redirect rails return a paymentFormUrl. The two are mutually exclusive — branch on whichever is present.
{
"id": "TXN-MT8E00EB-V5OK",
"status": "pending",
"amount": 100, // USD — what you charged
"currency": "USD",
"amountLocal": 1820.50, // MXN — what the buyer transfers
"currencyLocal": "MXN",
"paymentMethod": "spei",
"paymentData": {
"method": "spei",
"clabe": "706180000000000018",
"reference": "TXN-MT8E00EB-V5OK",
"bankName": "STP",
"dueDate": "2026-08-27T14:00:00Z"
},
"paymentFormUrl": null
}5. Receive the webhook
We POST payment.completed (or payment.failed) to your registered endpoint. Verify the signature before you trust the body — details in Host-to-Host › Reliability.
6. Reconcile
A webhook can be lost — your endpoint is down, a deploy is mid-flight, a proxy drops it. Treat GET /payments/{id} as the source of truth and poll any charge that stayed pending past its due date. Cash rails settle in hours or days; that is not a failure, it is how the rail works.