Create payment
/api/v1/paymentsDirect-charge flow — we return the upstream provider's paymentFormUrl for you to embed / redirect to.
amount you send is in USD (major units, e.g. 50 = US$50.00). There is no input currency field— Key2Pay converts USD → the buyer's local currency automatically (per country + method) and returns the local equivalent as amountLocal + currencyLocal. You always price and settle in USD. (The only exception is the refund, which takes the amount in the original tx's LOCAL currency.)userName, userEmail and documentId behave differently per flow. In the hosted-checkout / payment-link flowall three are required — the checkout asks the buyer for them so the "Datos del cliente" record is complete; if any is absent or malformed you get a 422 missing_required_fields with details.missingFields naming each one (userName · email · documentId), no transaction is created, and the hosted modal collects whatever's missing automatically. On a direct API call they are recommended but not enforced — pass them when you have them (some rails still need documentIdfor the upstream charge), but the platform won't reject the request for their absence. Either way, errors are never cached, so you may reuse the same Idempotency-Key on a retry.cascade_exhausted with reason no_provider_for_method_region — retry once before doing anything else. The cascade engine already does a force-reload of its internal state in this scenario, so a second attempt sees the same truth as your listing. If the retry also fails:details.paymentMethodEnabledForShop === false→ you hardcoded a code that isn't from YOUR shop. Call GET /api/v1/payment-methods again.details.paymentMethodEnabledForShop === true→ there's a real mismatch in our config; open a ticket with therequestIdand we'll unblock it in minutes.
paymentFormUrl(e.g. secure-int.key2pay.io/checkout?token=…). You redirect to or embed that URL. Gives you more control over the UI.checkoutUrl and handle all the per-method UI (QR, CLABE, redirect, voucher). You just redirect. 1 line of integration./api/v1/paymentssecret keyStarts a server-to-server charge. The shop is identified via the Bearer token (POST /api/v1/auth/token). Pass `paymentMethodId` with the 4-digit id returned by GET /api/v1/payment-methods. Each retailer / bank / native method has its own unique id (e.g. 1001 → SPEI, 1002 → OXXO, 1003 → Walmart, 1004 → 7-Eleven, 1005 → BBVA, 1006 → Scotiabank) and the cascade routes exactly to that upstream. The same `paymentMethodId` comes back in the response and is queryable via GET /api/v1/payments. The response includes `paymentFormUrl` (the upstream provider's URL — you redirect to or embed it). If you prefer our hosted UI, use POST /api/v1/checkout/sessions which returns its own `checkoutUrl`. The settlement crypto, the destination wallet, and the KYC level are derived internally from the merchant's config — they are not sent in the body. Note: the legacy path `POST /api/v1/transactions` works exactly the same; use whichever you prefer.
amountnumberrequiredALWAYS in USD (major units, e.g. 50 = US$50.00). There is no currency field: Key2Pay initiates every payment in USD and computes the buyer's local amount automatically (returned as amountLocal/currencyLocal). > 0, max 100,000.paymentMethodIdstringrequiredThe 4-digit code from GET /api/v1/payment-methods. Each retailer / bank / native method has its own unique code (e.g. "1001" SPEI, "1002" OXXO, "1003" Walmart, "1005" BBVA). The codes are OURS — assigned by the platform and stable even when we rotate upstream providers. This is the recommended field and the only one that guarantees deterministic routing to the specific rail.userNamestringRequired in the hosted-checkout / payment-link flow (the checkout collects it); recommended but not enforced on a direct API call. The buyer's full name (at least 2 words), max 120 chars. In the checkout flow, if absent or malformed the response is 422 `missing_required_fields` naming `userName`.userEmailstringRequired in the hosted-checkout / payment-link flow (the checkout collects it); recommended but not enforced on a direct API call. The buyer's email (valid address), max 254 chars. In the checkout flow, if absent or malformed the response is 422 `missing_required_fields` naming `email`.documentIdstringRequired in the hosted-checkout / payment-link flow (the checkout collects it); recommended on a direct API call (some rails — Brazil PIX, Mexico OXXO — still need it for the upstream charge). The buyer's identity document — RFC/CURP (Mexico), CPF (Brazil PIX, 11 digits), or CC/DNI/etc. elsewhere. Forwarded to the provider; not persisted on the tx.paymentMethodstringLegacy. Ambiguous slug (card · debit · spei · pix · oxxo · voucher · bank_transfer · …). Accepted only as a fallback when `paymentMethodId` is not sent. The cascade picks any provider in the slug + country bucket, without guaranteeing a specific retailer — for example `voucher` + MEX could route Walmart or 7-Eleven. Use `paymentMethodId` in new code.countrystringPayer's country. Accepts ISO-3 (BRA, MEX, COL, ARG, CHL, PER, …) or ISO-2 (BR, MX, CO, AR, CL, PE, …) interchangeably — we normalize to ISO-3 internally. Default: USA.userPhonestringPayer's phone (include the country code, e.g. +52). Some rails (OXXO, PIX, voucher) require it for the upstream charge.userIpstringPayer's IP (we also infer it from the request).merchantOrderIdstringYour own reference id (any string ≤120 chars). Stored on the transaction and queryable via GET /transactions?merchantOrderId=… for disaster-recovery lookup.hostedCheckoutbooleanWhen true, the response includes `checkoutUrl` — a Key2Pay-hosted page that renders QR / CLABE / redirect UI per the selected method. Redirect the customer there and listen for the webhook.returnUrlstringWhere the hosted checkout sends the customer once they finish (or click Return). Only used when `hostedCheckout: true`.merchantIdstringOnly if your token covers multiple merchants; default: the shop's.shopIdstringSame, for multiple shops.
curl https://sandbox.key2pay.ai/api/v1/payments \
-H "Authorization: Bearer sk_test_51N8mP...exampleK3Y" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 50,
"paymentMethodId": "sbx_spei",
"country": "MEX",
"userName": "Test Buyer",
"userEmail": "test@test.com",
"documentId": "TESTDOC12345",
"merchantOrderId": "ORD-12345"
}'
# userName + userEmail + documentId: required in the hosted-checkout/payment-link flow (else 422
# missing_required_fields); recommended but optional on a direct API call. Sent here for completeness.
# sandbox: use a sbx_ test method (sbx_pix/sbx_spei/sbx_card/sbx_pse/sbx_cash).
# production: use the 4-digit code from GET /payment-methods (e.g. 1001 SPEI).
# For OUR hosted checkout page add "hostedCheckout": true → response carries checkoutUrl
# instead of paymentFormUrl. See /docs/endpoints/hosted-checkout.{
"transactionId": "TXN-MVZQXW7B-A4F2",
"status": "pending",
"amount": 50.00,
"amountLocal": 882.17,
"currencyLocal": "MXN",
"paymentMethodId": "sbx_spei",
"paymentMethod": "spei",
"paymentMethodName": "SPEI",
"logoUrl": "https://api.key2pay.ai/api/payment-method-logo/spei__mex?v=2026-07-04T00:00:00.000Z",
"iconUrl": "https://api.key2pay.ai/api/payment-method-logo/spei__mex?v=2026-07-04T00:00:00.000Z",
"fees": {
"platform": 1.45,
"provider": 0,
"network": 0,
"total": 1.45
},
"settlement": {
"type": "delayed",
"delay": "T+2",
"reserve": 5,
"status": "pending"
},
"paymentFormUrl": "https://secure-int.key2pay.io/checkout?token=…",
"paymentData": { "method": "spei" },
"expiresAt": "2026-05-12T16:00:00.000Z"
}country format: we accept ISO-2 ("MX", "BR","CO", …) and ISO-3 ("MEX", "BRA","COL", …) interchangeably. Internally we normalize to ISO-3 before routing, so either format works the same. If you pass paymentMethodId without country, the country is inferred from the 4-digit id (each one is tied to a specific country)./api/v1/paymentssandboxamountintegerRequiredpaymentMethodIdstringRequiredcountryenumuserEmailstringuserNamestringhostedCheckoutstringcurl -X POST "https://sandbox.key2pay.ai/api/v1/api/v1/payments" \
-H "Authorization: Bearer sk_test_…YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50,
"paymentMethodId": "sbx_spei",
"country": "MEX",
"userEmail": "test@test.com",
"userName": "Test Customer",
"hostedCheckout": "false"
}'