Confirmación: hosted fields frente a redirección#
Crear el intent solo reserva la intención de cobro. El cobro empieza cuando confirmas:
| Método | Ruta | Autenticación | Nota |
|---|---|---|---|
| POST | /v1/payment-intents/{id}/confirm | sk_ · pk_ + X-Client-Secret | Cuerpo opcional. El único campo de medio de pago es payment_method_token. |
Hay exactamente dos formas de que el medio de pago llegue a la pasarela, y en ninguna de las dos pasa por tu servidor ni por el nuestro:
| Familia | El pagador introduce sus datos en | Tú envías | Estado resultante |
|---|---|---|---|
| Hosted fields | un iframe del proveedor incrustado en el checkout (los Bricks de MercadoPago) | payment_method_token | PROCESSING o SUCCEEDED |
| Redirección | el sitio del proveedor, en otra página (Webpay Plus) | cuerpo vacío | REQUIRES_ACTION con redirect_url |
La regla que gobierna todo lo demás: SAQ A#
Corolario práctico: si tu código tiene un número de tarjeta en la mano, la integración está mal montada. No existe un campo del contrato donde ponerlo, y añadirlo no está en la hoja de ruta.
Familia 1 · Hosted fields (token)#
El proveedor renderiza los campos dentro de su propio iframe, valida, tokeniza y te devuelve un token de un solo uso. Tú confirmas con ese token.
navegador (iframe del proveedor) tu servidor ApiPay Hub pasarela
datos de tarjeta -> tokeniza
token ------------------------->
POST /confirm -> PROCESSING -> cobra
SUCCEEDED <- aprueba
const intent = await apipay.paymentIntents.confirm('pi_9f2c4a7d1b3e4f60a8c5d2e7', {
payment_method_token: token, // emitido por la pasarela, nunca datos de tarjeta
});
// Este POST no se reintenta nunca de forma automatica: es un cobro.
if (intent.status === 'PROCESSING') {
// El resultado definitivo llega por webhook.
}Familia 2 · Redirección#
La pasarela exige que el pagador esté en su sitio, como en Webpay Plus. El primer confirm va sin
cuerpo y devuelve el intent en REQUIRES_ACTION con la redirect_url a la
que hay que enviar al pagador.
tu servidor ApiPay Hub navegador pasarela
POST /confirm -> REQUIRES_ACTION
redirect_url ------------> navega --------------> paga
<----------- vuelve a return_url con token
POST /confirm -> PROCESSING -> cobra ------------------------------> commit
SUCCEEDED <--------------------------------------- aprueba
// Primer confirm: sin cuerpo. La pasarela decide que hace falta una accion externa.
const intent = await apipay.paymentIntents.confirm('pi_9f2c4a7d1b3e4f60a8c5d2e7');
if (intent.status === 'REQUIRES_ACTION' && intent.redirect_url) {
// Redireccion de nivel superior, jamas dentro de un iframe tuyo: muchas pasarelas
// rompen el flujo si detectan que no son la ventana principal.
return res.redirect(303, intent.redirect_url);
}El segundo confirm#
Cuando el pagador vuelve a tu return_url, la pasarela añade un parámetro a la URL: en Webpay Plus es
token_ws, o TBK_TOKEN si el pagador anuló desde el formulario. Ese retorno es la señal para
confirmar por segunda vez, que es lo que hace el commit de la operación y lleva el intent de
REQUIRES_ACTION a PROCESSING.
Si usas @apipay/checkout-js, el SDK detecta esos parámetros en la URL de tu página y los reenvía al
widget, que dispara el segundo confirm con la pk_ y el X-Client-Secret. No tienes que hacer
nada, más allá de montar el widget también en la página de retorno.
Si integras server-to-server sin el widget, el segundo confirm lo haces tú con tu sk_. Los
detalles concretos de cada proveedor están en su guía: Webpay,
MercadoPago.
Las dos autenticaciones del confirm#
confirm es el único endpoint de escritura que acepta las dos credenciales:
| Credencial | Quién la usa | Cabeceras |
|---|---|---|
sk_test_ / sk_live_ | tu servidor | X-API-Key: sk_… |
pk_test_ / pk_live_ | el widget en el navegador | X-API-Key: pk_… y X-Client-Secret: pi_…_secret_… |
Con pk_ el permiso está limitado al intent cuyo client_secret se presenta: no puede leer otros
recursos, no puede cancelar y no puede reembolsar. Intentarlo responde
403 insufficient_permissions.
payment_method es null hoy, y es a propósito#
La respuesta del confirm incluye el campo payment_method. Hoy siempre viene null, en todas
las pasarelas y en todos los estados:
{
"id": "pi_9f2c4a7d1b3e4f60a8c5d2e7",
"status": "SUCCEEDED",
"payment_method": null,
"gateway_id": "webpay",
"livemode": true
}
No es un defecto pendiente: es la consecuencia directa de SAQ A. Ningún adaptador de pasarela captura
metadatos de tarjeta. El adaptador de Webpay, por ejemplo, ni siquiera enlaza a un DTO el campo
card_detail que Transbank devuelve con los últimos cuatro dígitos, para que ese dato no exista ni en
memoria dentro del proceso. Poblar payment_method exigiría revertir esa decisión y ampliar el
alcance de cumplimiento.
Qué te puede devolver un confirm#
| Resultado | Significado | Qué hacer |
|---|---|---|
200 con REQUIRES_ACTION | falta una acción del pagador | redirigir a redirect_url |
200 con PROCESSING | cobro en curso | esperar el webhook |
200 con SUCCEEDED | aprobado en línea | igual, esperar el webhook para marcar la orden |
402 card_declined | el emisor rechazó | mensaje al pagador e intent nuevo |
402 gateway_rejected | la pasarela rechazó | igual que el anterior |
409 invalid_state_transition | el intent ya no admite confirm | leer el intent y conciliar |
422 currency_not_supported | la pasarela no opera esa divisa para tu tenant | revisar la configuración del tenant |
503 gateway_unavailable | circuito abierto hacia la pasarela | respetar Retry-After y reintentar tú, con la misma Idempotency-Key si aplica |
Siguientes pasos#
- Webhooks: el resultado definitivo del cobro llega ahí.
- Payment intents: la matriz de transiciones completa.
- Modo test: reproduce
REQUIRES_ACTIONycard_declineda voluntad con el monto adecuado. - Manejo de errores: los 17
codey la forma del documentoproblem+json.