Endpoints

Create payment

POST/api/v1/payments

Direct-charge flow — we return the upstream provider's paymentFormUrl for you to embed / redirect to.

The operation currency is ALWAYS USD. Every payment is initiated in USD: the 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.
If you get 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 the requestIdand we'll unblock it in minutes.
Two flows, two endpoints — pick one:
Direct charge (this endpoint) — we return the upstream provider's paymentFormUrl(e.g. secure-int.key2pay.io/checkout?token=…). You redirect to or embed that URL. Gives you more control over the UI.
Hosted checkout POST /api/v1/checkout/sessions — we return OUR checkoutUrl and handle all the per-method UI (QR, CLABE, redirect, voucher). You just redirect. 1 line of integration.
POST/api/v1/paymentssecret key

Starts 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.

Body parameters
  • amountnumberrequired
    ALWAYS 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.
  • paymentMethodIdstringrequired
    The 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.
  • userNamestring
    Required 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`.
  • userEmailstring
    Required 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`.
  • documentIdstring
    Required 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.
  • paymentMethodstring
    Legacy. 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.
  • countrystring
    Payer'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.
  • userPhonestring
    Payer's phone (include the country code, e.g. +52). Some rails (OXXO, PIX, voucher) require it for the upstream charge.
  • userIpstring
    Payer's IP (we also infer it from the request).
  • merchantOrderIdstring
    Your own reference id (any string ≤120 chars). Stored on the transaction and queryable via GET /transactions?merchantOrderId=… for disaster-recovery lookup.
  • hostedCheckoutboolean
    When 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.
  • returnUrlstring
    Where the hosted checkout sends the customer once they finish (or click Return). Only used when `hostedCheckout: true`.
  • merchantIdstring
    Only if your token covers multiple merchants; default: the shop's.
  • shopIdstring
    Same, for multiple shops.
Request
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.
Response
{
  "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"
}
Not exposed:the settlement crypto, the destination wallet, the provider that processed the charge, the external provider's txId, or the internal cascade trace. Those details are internal — if the cascade changes tomorrow, your integration doesn't notice.
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).
Try itPOST/api/v1/paymentssandbox
Request body
amountintegerRequired
paymentMethodIdstringRequired
countryenum
userEmailstring
userNamestring
hostedCheckoutstring
Live snippet
curl -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"
}'
Snippet updates as you edit the form. Sandbox responses are deterministic.