Confirmas todos los métodos igual: winal.js con paymentMethod y
paymentToken. Lo que cambia es qué trae next_action cuando el
intent queda en requires_action — este es el shape REAL que devuelve la Api
hoy (verificado contra ConnectorMappings y el conector Sim).
| Método | Cómo se confirma | next_action.type | Latencia | Estado en fase 0 |
|---|---|---|---|---|
| Tarjeta | Token de tarjeta (tokenización client-side) | redirect (solo si pide 3DS) |
Instantánea | funciona hoy |
| SPEI | El pagador transfiere a una CLABE virtual | bank_transfer |
Minutos | funciona en pruebas |
| CoDi | El pagador escanea un QR con su banca móvil | display_qr |
Minutos | funciona en pruebas |
| DiMo | Push al alias telefónico del pagador (sin QR) | display_qr (qr_data siempre null) |
Minutos | celular como token |
| OXXO | El pagador paga en caja con una referencia | oxxo_voucher |
Minutos a días | funciona en pruebas |
| BNPL (Kueski/Aplazo/Mercado Crédito) | Redirección al checkout de aprobación de crédito del proveedor | redirect |
Minutos (aprobación de crédito) | diseñado, no ejecutable aún |
| Card-present (SmartPOS) | Token que entrega el lector EMV (chip/contactless) | redirect (solo si el lector pide 3DS) |
Instantánea | funciona en pruebas |
Instant (confirmas tú
mismo con "Simular pago entrante", así que es inmediato).
Las latencias de la tabla son las que aplican en producción según el método
(ConfirmationLatency: instantánea para tarjeta/wallet, minutos para
SPEI/CoDi/DiMo/OXXO en tiempo real, y hasta días para OXXO con corte diferido según el
proveedor conectado).
Tarjeta
Confirmas con paymentMethod: "card" y un paymentToken — en
pruebas, cualquiera de los tok_sim_* (tabla completa en
Modo de pruebas); en producción, el token que entrega el SDK
del proveedor tras tokenizar la tarjeta en el navegador. El PAN nunca toca tu servidor
ni Winal (regla de arquitectura, PCI SAQ A).
El resultado más común es directo: succeeded (captura inmediata) o un decline
duro/suave. Si el proveedor pide autenticación adicional (3DS), el intent queda en
requires_action con:
{ "type": "redirect", "url": "https://proveedor.mx/3ds/..." }
winal.js redirige automáticamente window.location a esa URL
cuando ve este tipo. También existe el flujo auth/capture separado (útil en POS): con
tok_sim_auth el intento queda authorized sin capturar, y tu
servidor dispara POST /v1/payment_intents/{id}/capture cuando confirmas
la venta — el intent permanece en processing hasta que el proveedor confirme
la captura.
winal.confirmPayment({
clientSecret, intentId,
paymentMethod: "card",
paymentToken: "tok_sim_ok",
mountEl: document.getElementById("winal-mount"),
onStatus: (status) => console.log(status),
});
Tarjeta REAL en el navegador (mountCardForm)
Todo lo anterior asume que ya tienes un paymentToken (en pruebas, un
tok_sim_*). Para tarjetas de verdad, ese token lo genera el SDK del proveedor
dentro del navegador del pagador — winal.mountCardForm automatiza esa
parte: pinta el formulario correcto según qué conector haya ganado el ruteo del tenant para
card y, al enviarlo, tokeniza y llama confirmPayment por ti (mismo
polling y mismo render de next_action de siempre).
await winal.mountCardForm({
intentId,
clientSecret,
mountEl: document.getElementById("winal-mount"),
onStatus: (status) => console.log(status),
});
// El form ya quedó montado y con su botón "Pagar" conectado: no hay nada más que llamar.
mountCardForm primero pide
GET /public/payment_intents/{id}/checkout-config?method=card con el header
X-Winal-Client-Secret (autenticación de capacidad; el secreto va en el header,
no en el query, para no filtrarse a logs/Referer) para saber qué conector ganó el ruteo:
-
Con Sim (o sin credenciales configuradas): pinta el mismo selector de
tok_sim_*que ves en la demo — sigue funcionando sin cuenta de proveedor. -
Con Mercado Pago configurado: carga
https://sdk.mercadopago.com/js/v2y monta los Secure Fields del SDK (mp.fields.create('cardNumber'|'expirationDate' |'securityCode').mount(...)) — cada campo sensible vive en un<iframe>que sirve Mercado Pago, así que el número de tarjeta y el CVV nunca tocan el DOM de tu página. Al enviar el form llamamp.fields.createCardToken({ cardholderName })y usa el token resultante comopaymentToken.
public_key (la única credencial que este endpoint puede
devolver — nunca un access_token ni ningún otro secreto). Tu backend solo ve el
token ya generado, igual que con tok_sim_* hoy.
SPEI
Confirmas con paymentMethod: "spei" (el token no aplica; envía cualquier
valor). El intent queda en requires_action con la CLABE virtual a la que debe
transferir el pagador:
{
"type": "bank_transfer",
"clabe": "646180473921058317",
"beneficiary": "SIM SPEI",
"expires_at": "2026-07-06T18:30:00Z"
}
La CLABE sintética de Sim usa el prefijo real de pruebas de STP (646180) más
12 dígitos aleatorios. winal.js pinta la CLABE y el beneficiario con botón de
copiar. La confirmación real llega cuando el pagador transfiere desde su banco: Winal se
entera por evidencia del proveedor (webhook o status query), nunca por timeout. En
pruebas, usa "Simular pago entrante".
CoDi
Confirmas con paymentMethod: "codi". El intent queda en requires_action
con un QR que el pagador escanea desde su banca móvil:
{
"type": "display_qr",
"qr_data": "SIM-CODI-sim_rtp_4af1c02e9b3d4f4e8f0a1b2c3d4e5f60",
"expires_at": "2026-07-05T18:40:00Z"
}
winal.js incluye su propio generador de códigos QR (byte-mode, ECC nivel L,
ISO/IEC 18004) y dibuja el QR en el navegador a partir de qr_data —
ningún servicio externo de por medio. También muestra el texto crudo con botón de copiar
por si el pagador no puede escanear. En producción, qr_data trae el payload
real que entregue el proveedor CoDi conectado.
DiMo
payment_tokenpayment_method: "dimo" envía el celular (10 dígitos) en
payment_token — es el "instrumento" del cobro. Si lo omites, el intento
falla con motivo del proveedor SIM_MISSING_PAYER_PHONE. Con el conector Sim
cualquier celular de 10 dígitos funciona (p. ej. 5512345678); DiMo real
llegará vía STP tras la homologación de Banxico y formalizará un campo dedicado.
Cuando esté completo, el patrón será igual a CoDi pero como push directo al alias
registrado del pagador, sin QR (qr_data siempre null):
{
"type": "display_qr",
"qr_data": null,
"expires_at": "2026-07-05T18:40:00Z"
}
DiMo real llegará vía STP tras la homologación de Banxico (prevista dic. 2026); hasta entonces solo existe en el conector Sim, y con la limitación descrita arriba.
OXXO
Confirmas con paymentMethod: "oxxo". El intent queda en requires_action
con una referencia para pagar en caja:
{
"type": "oxxo_voucher",
"reference": "48213097652014",
"barcode_url": null,
"expires_at": "2026-07-08T18:30:00Z"
}
En Sim, barcode_url siempre es null (un proveedor real conectado
sí lo entrega); winal.js solo pinta el código de barras si el valor existe.
El pagador paga en tienda y el proveedor notifica a Winal; en pruebas, simula el pago con
el botón del portal o el endpoint admin — ver Modo de pruebas.
BNPL (compra ahora, paga después)
Confirmas con paymentMethod: "bnpl" — un solo literal para los tres proveedores
(Kueski Pay, Aplazo, Mercado Crédito); cuál se usa lo decide el ruteo configurado del tenant, igual
que con tarjeta. El pagador aprueba un plan de pago diferido en el checkout del proveedor: el
intent queda en requires_action con una redirección, mismo next_action.type
que un challenge 3DS:
{ "type": "redirect", "url": "https://checkout-del-proveedor-bnpl.mx/aprobar/..." }
La aprobación o el rechazo del crédito se resuelve por webhook verificado + fetch o por consulta de estado (regla 5) — nunca por timeout local, igual que 3DS.
bnpl — no existe un
tok_sim_bnpl para probarlo localmente. Confirmar con payment_method: "bnpl"
hoy responde:
{ "error": { "code": "payment_intent.no_route",
"message": "No hay conector configurado para el método 'bnpl'." } }
Card-present (SmartPOS) y CoDi en mostrador
Confirmas con paymentMethod: "card_present". A diferencia de tarjeta en línea, el
paymentToken lo genera el lector físico (SmartPOS con chip EMV/contactless), no
el navegador — el PAN nunca sale del hardware certificado (misma regla 2 que tarjeta en línea). En
pruebas, los tokens de Sim:
| Token | Resultado |
|---|---|
emv_sim_ok | Chip, aprobado, captura inmediata |
emv_sim_ok_contactless | Contactless/NFC, aprobado, captura inmediata |
emv_sim_auth | Autoriza sin capturar (mismo flujo auth/capture que tarjeta) |
emv_sim_declined | Decline duro |
curl -s https://api.winal.com.mx/v1/payment_intents/{id}/confirm \
-H "Authorization: Bearer $SK" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{ "payment_token": "emv_sim_ok", "payment_method": "card_present" }'
Las terminales (número de serie, modelo, sucursal) se dan de alta y administran desde
portal → Terminales — no hay un endpoint público de /v1 para registrarlas; una
vez activa, su id es lo único que tu integración necesita.
CoDi en mostrador
CoDi cobrado desde una terminal física no es un método distinto: es el mismo
paymentMethod: "codi" de siempre — solo agrega metadata.terminal_id con el
id de la terminal al crear el intent, para que el QR quede asociado a ESA caja en tus
reportes. El next_action es idéntico al CoDi de pantalla:
curl -s https://api.winal.com.mx/v1/payment_intents \
-H "Authorization: Bearer $SK" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{ "amount_minor": 30000, "currency": "MXN",
"metadata": { "terminal_id": "5b90ab6a-4a73-44d9-8416-e7923d2d7c15" } }'
{
"type": "display_qr",
"qr_data": "SIM-CODI-sim_rtp_bc1473020b6c408da29d102bc5ff8431",
"expires_at": "2026-07-08T07:04:52.845164+00:00"
}
La decisión de reusar codi tal cual (en vez de inventar un método
codi_mostrador) es deliberada: la terminal es solo el origen del cobro, no un
método de pago distinto. El hardware EMV real (certificación PCI PTS) está gated; el módulo de
gestión de terminales (alta, estados, metadata) funciona hoy de punta a punta contra Sim.