Saltar al contenido
ApiPay Hub · Docs

Confirmación: hosted fields frente a redirección#

Crear el intent solo reserva la intención de cobro. El cobro empieza cuando confirmas:

MétodoRutaAutenticaciónNota
POST/v1/payment-intents/{id}/confirmsk_ · pk_ + X-Client-SecretCuerpo 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:

FamiliaEl pagador introduce sus datos enTú envíasEstado resultante
Hosted fieldsun iframe del proveedor incrustado en el checkout (los Bricks de MercadoPago)payment_method_tokenPROCESSING o SUCCEEDED
Redirecciónel sitio del proveedor, en otra página (Webpay Plus)cuerpo vacíoREQUIRES_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:

CredencialQuién la usaCabeceras
sk_test_ / sk_live_tu servidorX-API-Key: sk_…
pk_test_ / pk_live_el widget en el navegadorX-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#

ResultadoSignificadoQué hacer
200 con REQUIRES_ACTIONfalta una acción del pagadorredirigir a redirect_url
200 con PROCESSINGcobro en cursoesperar el webhook
200 con SUCCEEDEDaprobado en líneaigual, esperar el webhook para marcar la orden
402 card_declinedel emisor rechazómensaje al pagador e intent nuevo
402 gateway_rejectedla pasarela rechazóigual que el anterior
409 invalid_state_transitionel intent ya no admite confirmleer el intent y conciliar
422 currency_not_supportedla pasarela no opera esa divisa para tu tenantrevisar la configuración del tenant
503 gateway_unavailablecircuito abierto hacia la pasarelarespetar Retry-After y reintentar , 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_ACTION y card_declined a voluntad con el monto adecuado.
  • Manejo de errores: los 17 code y la forma del documento problem+json.