Core concepts

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, so paymentFormUrl is null. 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:

json
{
  "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": "..."
}
Optional tags: you can pass up to 3 free-form tags on 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.
What 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.)
You will always get one or the other. A charge that produced neither a hosted page nor payment instructions is unpayable, so we do NOT create it: you get a 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.)

json
// 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)." }
      ]
    }
  }
}
Retry safely with the same Idempotency-Key. A 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}.

javascript
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_paymentapplepaygooglepaycrypto

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