En casi todas las integraciones de pago existe el mismo momento de riesgo: el cliente completa el pago en la pasarela y vuelve al sitio del comercio. Es tentador marcar el pedido como pagado en ese instante, cuando el navegador llega a la «página de éxito». Es también la causa más frecuente de inconsistencias.
Por qué la redirección no es una confirmación
La redirección depende del navegador del cliente, y el navegador no es un canal confiable para confirmar dinero:
- El cliente puede cerrar la pestaña, quedarse sin batería o perder la conexión justo después de pagar. El pago existe, pero tu sistema nunca se entera.
- Los parámetros de la URL de retorno pueden ser manipulados si no están firmados y verificados en el servidor.
- Algunos medios de pago no se confirman de inmediato: el estado puede quedar pendiente y resolverse minutos u horas después.
La página de retorno sirve para la experiencia del usuario («estamos procesando tu pago»), pero el estado del pedido debe cambiar a partir de una fuente verificable del lado del servidor.
Las dos fuentes confiables
Las pasarelas suelen ofrecer dos mecanismos que se complementan:
| Mecanismo | Cómo funciona | Para qué sirve |
|---|---|---|
| Notificación servidor a servidor (webhook) | La pasarela llama a una URL de tu servidor cuando cambia el estado de un pago. | Enterarse rápido de los cambios sin preguntar constantemente. |
| Consulta de estado por API | Tu servidor pregunta a la pasarela el estado de un pago concreto. | Verificar, recuperar notificaciones perdidas y resolver dudas. |
Lo recomendable es usar ambos: el webhook como mecanismo principal y la consulta por API como verificación y como red de seguridad cuando una notificación no llega. Los nombres y la disponibilidad exacta de cada mecanismo dependen de la pasarela, por lo que siempre hay que revisar su documentación.
Modela el pago como una máquina de estados
Cada pasarela usa sus propios nombres de estado. Conviene traducirlos a un conjunto interno pequeño y definir qué transiciones están permitidas. Así, una notificación que intenta llevar un pago de «pagado» a «pendiente» se detecta como anómala en lugar de aplicarse a ciegas.
| Estado interno | Significado | Puede pasar a |
|---|---|---|
pendiente | Se inició el pago y aún no hay resultado. | autorizado, pagado, rechazado, expirado |
autorizado | Fondos reservados, aún no cobrados (si la pasarela separa ambos pasos). | pagado, anulado |
pagado | Cobro confirmado. | reembolsado, reembolsado_parcial |
rechazado | El pago no fue aprobado. | (estado final) |
expirado | El cliente no completó el pago a tiempo. | (estado final) |
anulado | Autorización cancelada antes del cobro. | (estado final) |
reembolsado | Se devolvió el total. | (estado final) |
Los estados de la tabla son un ejemplo: el conjunto real depende de los medios de pago y de lo que reporte cada pasarela. Lo importante es que la tabla de transiciones exista y que el código la respete.
Cómo recibir un webhook correctamente
- Verificar autenticidadFirma, token o mecanismo que provea la pasarela.
- Guardar el eventoRegistrar el contenido recibido antes de procesarlo.
- Responder rápidoDevolver un código de éxito en pocos segundos.
- Procesar en segundo planoActualizar pedido, stock y notificaciones.
- Descartar duplicadosUsar el identificador del evento o del pago.
Responder rápido es importante: si tu servidor tarda, la pasarela puede considerar que la entrega falló y reenviarla, generando más duplicados. Por eso el trabajo pesado (actualizar inventario, enviar correos, registrar en el ERP) se hace después de responder.
Un esquema simplificado, en pseudocódigo:
recibirWebhook(solicitud):
si no firmaValida(solicitud): responder 401 y terminar
evento = parsear(solicitud.cuerpo)
// la restricción única en id_evento evita registrar dos veces lo mismo
insertado = guardarSiNoExiste(tabla_eventos, evento.id, solicitud.cuerpo)
responder 200
si insertado: encolar(procesarEvento, evento.id)
procesarEvento(idEvento):
evento = leer(tabla_eventos, idEvento)
pago = buscarPago(evento.referencia) // con bloqueo de fila
nuevo = traducirEstado(evento.estado)
si transicionPermitida(pago.estado, nuevo):
actualizarEstado(pago, nuevo)
ejecutarEfectos(pago, nuevo) // confirmar pedido, stock, aviso
si no:
registrarAnomalia(pago, evento) // revisar o consultar por API
marcarProcesado(idEvento)
Idempotencia: en ambos sentidos
Al recibir
Las pasarelas pueden enviar la misma notificación más de una vez, y es su comportamiento esperado. La defensa es guardar el identificador del evento con una restricción única en la base de datos y hacer que los efectos sean idempotentes: confirmar un pedido que ya está confirmado no debe hacer nada.
Al enviar
Cuando tu sistema crea un cobro o un reembolso y la respuesta no llega (por ejemplo, por un tiempo de espera), no sabes si la operación se realizó. Reintentar a ciegas puede cobrar dos veces. Si la pasarela admite una clave de idempotencia, envía siempre la misma clave al reintentar la misma operación. Si no la admite, consulta el estado por API antes de reintentar.
Generar la clave de idempotencia en cada intento. La clave debe identificar la operación de negocio (por ejemplo, «reembolso del pedido 1042») y guardarse antes del primer intento, para reutilizarla en los siguientes.
Eventos desordenados
No hay garantía de que las notificaciones lleguen en el orden en que ocurrieron. Un «pagado» puede llegar antes que un «autorizado». Con la máquina de estados, el evento atrasado simplemente no produce una transición válida y se ignora o se registra. Cuando haya dudas, la regla es simple: consultar el estado actual por API y usar esa respuesta como referencia.
Cuando el webhook no llega
Las notificaciones pueden perderse por una caída de tu servidor, un certificado vencido o un cambio de configuración. Un proceso programado debe buscar pagos que siguen en pendiente más allá de un tiempo razonable y consultar su estado en la pasarela. Este proceso convierte una notificación perdida en un retraso, no en un pedido huérfano.
Conciliación
Aun con webhooks e idempotencia bien implementados, conviene comparar periódicamente lo que tu sistema registró con los reportes de la pasarela. La conciliación detecta problemas que ningún mecanismo automático ve: configuraciones erróneas, cambios en la pasarela o errores humanos.
| Diferencia | Posible causa | Acción |
|---|---|---|
| Pago aprobado en la pasarela, pedido pendiente | Webhook perdido o procesamiento fallido. | Consultar estado y reprocesar. |
| Pedido pagado, sin pago en la pasarela | Confirmación basada en la redirección o error de código. | Revisar urgente: posible entrega sin cobro. |
| Montos distintos | Errores de redondeo, moneda o cargos parciales. | Revisar cálculo en unidades mínimas de la moneda. |
| Reembolso en la pasarela no registrado | Reembolso hecho desde el panel de la pasarela. | Registrar y definir el flujo oficial de reembolsos. |
Errores frecuentes
- Confirmar el pedido en la página de retorno.
- No verificar la firma o la autenticidad del webhook.
- Procesar todo antes de responder a la pasarela, provocando reintentos.
- No guardar el contenido original de los eventos, lo que impide investigar después.
- Usar números decimales de punto flotante para montos.
- Registrar datos de tarjeta o credenciales en los logs.
- No tener un proceso para pagos que quedan pendientes.
Lista de verificación
- El estado del pedido cambia solo por fuentes del lado del servidor.
- Los webhooks se verifican, se guardan y se responden rápido.
- Existe una restricción única sobre el identificador de evento.
- Hay una tabla de transiciones de estado y se respeta.
- Las operaciones salientes usan claves de idempotencia o consulta previa.
- Un proceso programado revisa los pagos pendientes.
- Se concilia periódicamente con los reportes de la pasarela.
Preguntas frecuentes
¿Puedo usar solo la consulta por API, sin webhooks?
Es posible, consultando periódicamente los pagos pendientes, pero introduce demoras y más llamadas a la pasarela. Los webhooks permiten reaccionar rápido; la consulta sirve como verificación y respaldo.
¿Qué código debo responder si el evento ya fue procesado?
Un código de éxito. Para la pasarela, la entrega fue correcta; que tu sistema ya conociera el evento es un detalle interno.
¿Qué hago si la firma no es válida?
Rechazar la solicitud, no procesarla y registrar el intento. Si ocurre con frecuencia, revisar la configuración del secreto o la posibilidad de solicitudes falsificadas.
Este tema se complementa con cómo consumir servicios externos sin detener tu operación, que explica tiempos de espera y reintentos en las llamadas salientes.