Collecting the payment
How a created payment is actually collected: the paymentData block, the paymentFormUrl redirect for direct charge, and the hosted-checkout alternative.
POST /api/v1/payments creates a pending transaction and gives you one of two ways to collect it, depending on the rail:
- Direct rails (SPEI, OXXO, cash vouchers, PIX…) return the buyer's payment instructions inline on
paymentData— the CLABE + reference, the barcode, the QR. There is no hosted page, sopaymentFormUrlisnull. You render these in your own UI. - Hosted rails (card forms, bank redirects) return a
paymentFormUrl— redirect the buyer there.
The response envelope
A direct rail — SPEI. Note paymentFormUrl: null and the instructions inline:
{
"transactionId": "TXN-...",
"status": "pending",
"paymentMethodId": "1221",
"amount": 50.00,
"amountLocal": 882.17,
"currencyLocal": "MXN",
"paymentFormUrl": null,
"paymentData": {
"method": "spei",
"clabe": "703428043000024977",
"reference": "1303628",
"bankName": "tesored",
"beneficiaryName": "CLB Payment",
"dueDate": "2026-08-04T09:13:20.381Z",
"methodName": "SPEI"
},
"tag1": "campaign:black-friday",
"tag2": null,
"tag3": null,
"expiresAt": "..."
}POST /payments — tag1, tag2, tag3 (≤200 chars each). Stash any info of your interest (order refs, campaign ids, internal notes). They're stored on the transaction and echoed back verbatim on this response and on GET /payments (both the single-transaction and the list endpoints). Pure pass-through: they never affect routing, pricing, or the provider — and they behave identically in sandbox and production.paymentData contains: always the resolved method slug, plus — for a direct-integration rail — the buyer's actual payment instructions as the provider returned them: clabe + reference for SPEI, barcode for OXXO / vouchers, qrCode for PIX, plus bankName, beneficiaryName and a per-method dueDate when the rail provides them. Render these in your own UI — for these rails paymentFormUrl is null because there is no hosted page to redirect to. Rails that DO use a hosted page return paymentFormUrl instead. The same block is available on GET /payments/{id} if you need to re-fetch it. (The top-level expiresAt in the envelope is a short placeholder, not the per-method deadline — use paymentData.dueDate.)502 payment_instructions_unavailable naming the rail instead of a pending transaction your buyer can't pay. It is not a missing field in your request — retry, and if it persists report the details.provider value.Smart missing-data recovery
Some rails need an extra buyer field — Brazil PIX needs a CPF, Mexico OXXO needs an RFC/CURP + phone, India UPI needs a VPA. If a required field is missing or malformed, the charge does NOT fail opaquely: you get a 422 missing_required_fields whose details.missingFields (or malformedFields) names exactly what to collect — each with a key, type, label and validation rule. Collect it, retry the same charge, and it goes through. If the data is already valid, the charge proceeds directly — no extra round-trip. (Our hosted checkout turns this into an elegant modal automatically.)
// POST /payments for Brazil PIX without the CPF →
{
"error": {
"code": "missing_required_fields",
"type": "invalid_request_error",
"message": "This payment method requires: Documento de identidad.",
"details": {
"reason": "missing",
"missingFields": [
{ "key": "documentId", "type": "document",
"label": "Documento de identidad",
"help": "Verifica el documento (RFC 12-13, CURP 18, CPF 11 dígitos)." }
]
}
}
}missing_required_fields error is never cached, so resending the corrected body (the field added) under the same key is NOT a 409 — it's a fresh attempt that charges. No tx is created on the error path, so nothing leaks.Direct charge → redirect the buyer
For the direct-charge flow, send the buyer to paymentFormUrl (redirect, open in a new tab, or embed in an iframe). They complete the payment on the provider's page; you learn the outcome from the webhook (payment.completed / payment.failed) or by polling GET /payments/{id}.
const res = await api.post("/payments", {
amount: 50, paymentMethodId: "sbx_spei", country: "MEX", userEmail: "test@test.com",
});
// Direct charge: redirect the buyer to the provider's hosted page.
window.location.href = res.paymentFormUrl;
// Then react to the payment.completed webhook (or poll GET /payments/{id}).Method slugs
paymentData.method is one of the canonical method slugs. Switch on it if you want method-aware copy, but you don't need to render per-method UI — the provider's page does that.
carddebitbank_transferwirespeipixoxxoboletovoucheronline_paymentapplepaygooglepaycryptoPrefer not to redirect to the provider? Use hosted checkout
POST /api/v1/checkout/sessions returns a single checkoutUrl on OUR domain — we render the method picker + the per-method UI (CLABE/QR/voucher) for you, and the buyer never sees the upstream provider. See the hosted-checkout doc for the full flow.