Lista de verificación para webhooks de pago idempotentes y actualizaciones de facturación seguras
Lista de verificación para webhooks de pago idempotentes: deduplicar eventos, manejar reintentos y actualizar facturas, suscripciones y accesos de forma segura.

Por qué los webhooks de pago generan actualizaciones duplicadas
Un webhook de pago es un mensaje que tu proveedor de pagos envía a tu backend cuando ocurre algo importante, como que un cargo se confirma, una factura se paga, una suscripción se renueva o se emite un reembolso. Básicamente es el proveedor diciendo: “Esto pasó. Actualiza tus registros.”
Los duplicados ocurren porque la entrega de webhooks está diseñada para ser fiable, no para ocurrir exactamente una vez. Si tu servidor va lento, hace timeout, devuelve un error o está brevemente indisponible, el proveedor normalmente reintentará el mismo evento. También puedes ver dos eventos distintos que se refieran a la misma acción del mundo real (por ejemplo, un evento de invoice y un evento de payment vinculados a un mismo pago). Los eventos también pueden llegar fuera de orden, especialmente con seguimientos rápidos como reembolsos.
Si tu manejador no es idempotente, puede aplicar el mismo evento dos veces, lo que se convierte en problemas que clientes y equipos financieros notan de inmediato:
- Una factura marcada como pagada dos veces, creando entradas contables duplicadas
- Una renovación aplicada dos veces, extendiendo el acceso demasiado
- Concesiones de acceso duplicadas (créditos extra, asientos o características)
- Reembolsos o contracargos que no revierte el acceso correctamente
Esto no es solo “mejor práctica”. Es la diferencia entre una facturación que inspira confianza y otra que genera tickets de soporte.
El objetivo de esta lista de verificación es simple: trata cada evento entrante como “aplicar como máximo una vez”. Guardarás un identificador estable para cada evento, manejarás reintentos de forma segura y actualizarás facturas, suscripciones y entitlements de manera controlada. Si construyes el backend en una herramienta sin código como AppMaster, las mismas reglas siguen aplicando: necesitas un modelo de datos claro y un flujo de handler repetible que se mantenga correcto bajo reintentos.
Conceptos básicos de idempotencia que puedes aplicar a webhooks
Idempotencia significa que procesar la misma entrada más de una vez produce el mismo estado final. En términos de facturación: una factura termina pagada una vez, una suscripción se actualiza una vez y el acceso se concede una vez, incluso si el webhook se entrega dos veces.
Los proveedores reintentan cuando tu endpoint hace timeout, devuelve un 5xx o la red cae. Esos reintentos repiten el mismo evento. Eso es distinto de un evento separado que representa un cambio real, como un reembolso días después. Los eventos nuevos tienen IDs distintos.
Para que esto funcione, necesitas dos cosas: identificadores estables y una pequeña “memoria” de lo que ya viste.
Qué IDs importan (y qué almacenar)
La mayoría de plataformas de pago incluyen un ID de evento que es único para el evento del webhook. Algunas también incluyen un request ID, idempotency key o un ID único del objeto de pago (como un charge o payment intent) dentro del payload.
Almacena lo que te ayuda a responder a esta pregunta: “¿Ya apliqué exactamente este evento?”
Un mínimo práctico:
- Event ID (clave única)
- Tipo de evento (útil para depuración)
- Timestamp de recepción
- Estado de procesamiento (processed/failed)
- Referencia al cliente, factura o suscripción afectada
La jugada clave es almacenar el ID del evento en una tabla con una restricción única. Entonces tu manejador puede hacer esto de forma segura: insertar el ID del evento primero; si ya existe, parar y devolver 200.
Cuánto tiempo mantener registros de dedupe
Conserva los registros de dedupe el tiempo suficiente para cubrir reintentos tardíos e investigaciones de soporte. Una ventana común es de 30 a 90 días. Si tratas con contracargos, disputas o ciclos de suscripción largos, guárdalos más tiempo (6 a 12 meses) y depura filas antiguas para que la tabla siga rápida.
En un backend generado como AppMaster, esto se mapea limpiamente a un modelo simple WebhookEvents con un campo único en el ID del evento, además de un Business Process que sale temprano cuando se detecta un duplicado.
Diseña un modelo de datos simple para deduplicar eventos
Un buen manejador de webhooks es mayormente un problema de datos. Si puedes registrar cada evento del proveedor exactamente una vez, todo lo que sigue se vuelve más seguro.
Empieza con una tabla que actúe como un registro de recibos. En PostgreSQL (incluyendo cuando se modela en AppMaster Data Designer), mantenla pequeña y estricta para que los duplicados fallen rápido.
Lo mínimo que necesitas
Aquí tienes una base práctica para una tabla webhook_events:
provider(text, por ejemplo "stripe")provider_event_id(text, requerido)status(text, por ejemplo "received", "processed", "failed")processed_at(timestamp, nullable)raw_payload(jsonb o text)
Añade una restricción única en (provider, provider_event_id). Esa regla única es tu principal guardarraíl de dedupe.
También querrás los IDs de negocio que usas para localizar los registros a actualizar. Estos son distintos del ID del evento del webhook.
Ejemplos comunes incluyen customer_id, invoice_id y subscription_id. Mantenlos como texto porque los proveedores suelen usar IDs no numéricos.
Payload crudo vs campos parseados
Almacena el payload crudo para poder depurar y reprocesar después. Los campos parseados facilitan las consultas y reportes, pero solo guarda lo que realmente usas.
Un enfoque simple:
- Siempre guarda
raw_payload - También guarda algunos IDs parseados que consultas con frecuencia (customer, invoice, subscription)
- Guarda un
event_typenormalizado (texto) para filtrar
Si llega un evento invoice.paid dos veces, tu restricción única bloquea la segunda inserción. Aún tienes el payload crudo para auditoría, y el invoice ID parseado facilita ubicar la factura que actualizaste la primera vez.
Paso a paso: un flujo seguro para el handler de webhooks
Un handler seguro es aburrido a propósito. Se comporta igual cada vez, incluso cuando el proveedor reintenta el mismo evento o entrega eventos fuera de orden.
El flujo de 5 pasos a seguir siempre
-
Verifica la firma y parsea el payload. Rechaza peticiones que fallen las comprobaciones de firma, tengan un tipo de evento inesperado o no puedan parsearse.
-
Escribe el registro del evento antes de tocar los datos de facturación. Guarda el provider event ID, tipo, tiempo de creación y el raw payload (o un hash). Si el ID del evento ya existe, trátalo como duplicado y detente.
-
Mapea el evento a un único registro “propietario”. Decide qué vas a actualizar: factura, suscripción o cliente. Almacena IDs externos en tus registros para poder buscarlos directamente.
-
Aplica un cambio de estado seguro. Solo avanza el estado. No deshagas una factura pagada porque llegue tarde un
invoice.updated. Registra lo que aplicaste (estado anterior, nuevo estado, timestamp, event ID) para auditoría. -
Responde rápido y registra el resultado. Devuelve éxito una vez que el evento esté almacenado de forma segura y procesado o ignorado. Registra si fue procesado, dedupeado o rechazado, y por qué.
En AppMaster, esto suele convertirse en una tabla de base de datos para eventos de webhook más un Business Process que verifica “¿ID de evento visto?” y luego ejecuta los pasos mínimos de actualización.
Manejo de reintentos, timeouts y entregas fuera de orden
Los proveedores reintentan webhooks cuando no obtienen una respuesta rápida de éxito. También pueden enviar eventos fuera de orden. Tu manejador debe permanecer seguro cuando la misma actualización llega dos veces, o cuando llega primero una actualización posterior.
Una regla práctica: responde rápido, haz el trabajo después. Trata la petición del webhook como un recibo, no como el lugar para ejecutar lógica pesada. Si llamas a APIs de terceros, generas PDFs o recalculas cuentas dentro de la petición, aumentas los timeouts y provocas más reintentos.
Fuera de orden: conserva la verdad más reciente
La entrega fuera de orden es normal. Antes de aplicar cualquier cambio, usa dos comprobaciones:
- Compara timestamps: solo aplica un evento si es más reciente que lo que ya tienes almacenado para ese objeto (factura, suscripción, entitlement).
- Usa prioridad de estados cuando los timestamps estén cerca o no sean claros: paid vence a open, canceled vence a active, refunded vence a paid.
Si ya registraste una factura como pagada y llega tarde un evento “open”, ignóralo. Si recibiste “canceled” y más tarde aparece un “active” más antiguo, mantén canceled.
Ignorar vs poner en cola
Ignora un evento cuando puedas probar que es obsoleto o ya aplicado (mismo event ID, timestamp más antiguo, prioridad de estado menor). Pon en cola un evento cuando dependa de datos que aún no tienes, como una actualización de suscripción que llega antes de que exista el registro de cliente.
Un patrón práctico:
- Almacena el evento inmediatamente con un estado de procesamiento (received, processing, done, failed)
- Si faltan dependencias, márcalo como waiting y reintenta en background
- Establece un límite de reintentos y alerta tras fallos repetidos
En AppMaster, esto encaja bien con una tabla de webhook events y un Business Process que reconoce la petición rápidamente y procesa eventos en cola de forma asíncrona.
Actualizar facturas, suscripciones y entitlements de forma segura
Una vez que manejaste la deduplicación, el siguiente riesgo es el estado partido: la factura dice pagada pero la suscripción sigue vencida, o el acceso se concedió dos veces y nunca se revocó. Trata cada webhook como una transición de estado y aplícala en una sola actualización atómica.
Facturas: haz que los cambios de estado sean monotónicos
Las facturas pueden pasar por estados como paid, voided y refunded. También puedes ver pagos parciales. No “actives/desactives” una factura basándote en el último evento que llegó. Almacena el estado actual más totales clave (amount_paid, amount_refunded) y solo permite transiciones seguras hacia adelante.
Reglas prácticas:
- Marca una factura como pagada solo una vez, la primera vez que veas un evento de paid.
- Para reembolsos, incrementa
amount_refundedhasta el total de la factura; nunca lo disminuyas. - Si una factura es voided, detén acciones de cumplimiento, pero conserva el registro para auditoría.
- Para pagos parciales, actualiza montos sin conceder beneficios de “totalmente pagado”.
Suscripciones y entitlements: concede una vez, revoca una vez
Las suscripciones incluyen renovaciones, cancelaciones y períodos de gracia. Mantén el estado de la suscripción y los límites de periodo (current_period_start/end), y deriva las ventanas de entitlement a partir de esos datos. Los entitlements deberían ser registros explícitos, no un booleano suelto.
Para control de acceso:
- Una concesión de entitlement por usuario por producto por periodo
- Un registro de revocación cuando el acceso termina (cancelación, reembolso, contracargo)
- Un rastro de auditoría que registre qué evento del webhook causó cada cambio
Usa una transacción para evitar estados partidos
Aplica actualizaciones de factura, suscripción y entitlement en una sola transacción de base de datos. Lee las filas actuales, verifica si este evento ya fue aplicado y luego escribe todos los cambios juntos. Si algo falla, haz rollback para no acabar con “factura pagada” pero “sin acceso” o a la inversa.
En AppMaster, esto suele mapear a un Business Process que actualiza PostgreSQL en una ruta controlada única y escribe una entrada de auditoría junto al cambio de negocio.
Controles de seguridad y protección de datos para endpoints de webhook
La seguridad de los webhooks es parte de la corrección. Si un atacante puede golpear tu endpoint, puede intentar crear estados falsos de “pagado”. Incluso con deduplicación, aún necesitas probar que el evento es real y mantener seguros los datos del cliente.
Verifica el remitente antes de tocar datos de facturación
Valida la firma en cada petición. Para Stripe, eso normalmente significa comprobar el encabezado Stripe-Signature, usando el cuerpo bruto de la petición (no un JSON reescrito) y rechazar eventos con timestamps antiguos. Trata la ausencia de encabezados como un fallo grave.
Valida lo básico temprano: método HTTP correcto, Content-Type y campos requeridos (event id, type y el object id que usarás para localizar una factura o suscripción). Si construyes esto en AppMaster, guarda el secret de firma en variables de entorno o configuración segura, nunca en la base de datos ni en código cliente.
Una lista rápida de seguridad:
- Rechaza peticiones sin firma válida y timestamp reciente
- Requiere encabezados y content type esperados
- Usa acceso a la base de datos con privilegios mínimos para el handler de webhooks
- Almacena secretos fuera de las tablas (env/config), rótalos cuando sea necesario
- Devuelve 2xx solo después de persistir el evento de forma segura
Mantén logs útiles sin filtrar secretos
Registra lo suficiente para depurar reintentos y disputas, pero evita valores sensibles. Guarda un subconjunto seguro de PII: provider customer ID, ID interno de usuario y quizá un email enmascarado (por ejemplo a***@domain.com). Nunca almacenes datos completos de tarjeta, direcciones completas ni headers de autorización crudos.
Registra lo que te ayuda a reconstruir qué pasó:
- Provider event id, tipo, tiempo de creación
- Resultado de verificación (firma ok/failed) sin guardar la firma
- Decisión de dedupe (nuevo vs ya procesado)
- IDs internos tocados (invoice/subscription/entitlement)
- Razón del error y conteo de reintentos (si pones en cola reintentos)
Añade protección básica contra abuso: rate limit por IP y (cuando sea posible) por customer ID, y considera permitir solo rangos IP conocidos del proveedor si tu infraestructura lo soporta.
Errores comunes que causan cargos/acc esos duplicados
La mayoría de bugs de facturación no son por matemáticas. Ocurren cuando tratas la entrega de un webhook como un mensaje único y fiable.
Errores que con más frecuencia causan actualizaciones duplicadas:
- Deduplicar por timestamp o cantidad en lugar de por event ID. Eventos diferentes pueden compartir la misma cantidad, y los reintentos pueden llegar minutos después. Usa el ID de evento único del proveedor.
- Actualizar la base de datos antes de verificar la firma. Verifica primero, luego parsea y actúa.
- Tratar cada evento como la única fuente de la verdad sin comprobar el estado actual. No marques ciegamente una factura como pagada si ya está pagada, reembolsada o void.
- Crear múltiples entitlements para la misma compra. Los reintentos pueden crear filas duplicadas. Prefiere un upsert tipo “asegura que exista entitlement para subscription_id” y luego actualiza fechas/límites.
- Fallar el webhook porque un servicio de notificaciones está caído. Email, SMS, Slack o Telegram no deberían bloquear la facturación. Pon en cola las notificaciones y devuelve éxito después de que los cambios de facturación centrales estén almacenados de forma segura.
Un ejemplo sencillo: llega un evento de renovación dos veces. La primera entrega crea una fila de entitlement. El reintento crea una segunda fila y tu app ve “dos entitlements activos” y concede asientos o créditos extra.
En AppMaster, la solución suele ser sobre el flujo: verifica primero, inserta el registro del evento con una restricción única, aplica actualizaciones de facturación con comprobaciones de estado y envía efectos secundarios (emails, recibos) a pasos asíncronos para que no puedan provocar una tormenta de reintentos.
Ejemplo realista: renovación duplicada + reembolso posterior
Este patrón parece aterrador, pero es manejable si tu handler es seguro para reejecuciones.
Un cliente está en un plan mensual. Stripe envía un evento de renovación (por ejemplo, invoice.paid). Tu servidor lo recibe, actualiza la base de datos, pero tarda demasiado en devolver 200 (cold start, base de datos ocupada). Stripe asume que falló y reintenta el mismo evento.
En la primera entrega, concedes acceso. En el reintento, detectas que es el mismo evento y no haces nada. Más tarde, llega un evento de reembolso (por ejemplo, charge.refunded) y revocas el acceso una vez.
Aquí hay una forma simple de modelar el estado en tu base de datos (tablas que puedes crear en AppMaster Data Designer):
webhook_events(event_id UNIQUE, type, processed_at, status)invoices(invoice_id UNIQUE, subscription_id, status, paid_at, refunded_at)entitlements(customer_id, product, active, valid_until, source_invoice_id)
Cómo debería quedar la base de datos después de cada evento
Después del Evento A (renovación, primera entrega): webhook_events obtiene una fila nueva para event_id=evt_123 con status=processed. invoices queda marcada como pagada. entitlements.active=true y valid_until avanza un periodo de facturación.
Después del Evento A otra vez (renovación, reintento): la inserción en webhook_events falla (unique event_id) o tu manejador detecta que ya fue procesado. No hay cambios en invoices ni entitlements.
Después del Evento B (reembolso): una fila nueva en webhook_events para event_id=evt_456. invoices.refunded_at se establece y status=refunded. entitlements.active=false (o valid_until se pone a ahora) usando source_invoice_id para revocar el acceso correcto una sola vez.
El detalle importante es el orden: la comprobación de dedupe ocurre antes de cualquier escritura de grant o revoke.
Lista rápida antes del lanzamiento
Antes de activar webhooks en producción, quieres pruebas de que un evento real actualice registros de facturación exactamente una vez, incluso si el proveedor lo envía dos (o diez) veces.
Usa esta lista para validar tu configuración de punta a punta:
- Confirma que cada evento entrante se guarda primero (raw payload, event id, tipo, tiempo de creación y resultado de verificación de firma), incluso si pasos posteriores fallan.
- Verifica que los duplicados se detecten temprano (mismo provider event id) y que el handler salga sin cambiar invoices, suscripciones ni entitlements.
- Prueba que la actualización de negocio sea única: un cambio de estado de factura, un cambio de estado de suscripción, una concesión o revocación de entitlement.
- Asegúrate de que los fallos se registren con suficiente detalle para reproducirlos de forma segura (mensaje de error, paso que falló, estado de reintento).
- Comprueba que tu handler devuelve respuesta rápido: reconoce la recepción una vez almacenada y evita trabajo pesado dentro de la petición.
No necesitas una gran plataforma de observabilidad para empezar, pero sí señales. Monitorea estos indicadores desde logs o dashboards simples:
- Pico en entregas duplicadas (a menudo normal, pero saltos grandes pueden indicar timeouts o problemas del proveedor)
- Alta tasa de error por tipo de evento (por ejemplo, fallo en pago de invoice)
- Backlog creciente de eventos atascados en reintentos
- Comprobaciones de inconsistencia (factura pagada pero sin entitlement, suscripción revocada pero acceso aún activo)
- Aumento repentino en el tiempo de procesamiento
Si construyes esto en AppMaster, mantén el almacenamiento de eventos en una tabla dedicada en Data Designer y convierte “marcar como procesado” en un punto de decisión único y atómico en tu Business Process.
Siguientes pasos: prueba, monitoriza y constrúyelo en un backend sin código
Las pruebas son donde la idempotencia se demuestra. No te quedes solo con el happy path. Reproduce el mismo evento varias veces, envía eventos fuera de orden y fuerza timeouts para que el proveedor reintente. La segunda, tercera y décima entrega no deberían cambiar nada.
Planifica también la reprocesación temprana. Tarde o temprano querrás reprocesar eventos pasados tras una corrección de bug, un cambio de esquema o un incidente del proveedor. Si tu handler es verdaderamente idempotente, el reprocesado se convierte en “reproducir eventos por el mismo pipeline” sin crear duplicados.
Soporte necesita además un pequeño runbook para que los problemas no se conviertan en conjeturas:
- Encuentra el event ID y comprueba si está registrado como procesado.
- Revisa la factura o el registro de suscripción y confirma el estado y timestamps esperados.
- Revisa el registro de entitlement (qué acceso se concedió, cuándo y por qué).
- Si hace falta, reejecuta el procesamiento para ese único event ID en un modo de reprocesado seguro.
- Si los datos están inconsistentes, aplica una acción correctiva única y regístrala.
Si quieres implementar esto sin escribir mucho boilerplate, AppMaster te permite modelar las tablas principales y construir el flujo de webhooks en un Business Process visual, generando código fuente real para el backend.
Intenta construir el handler de webhooks de punta a punta en un backend generado sin código y comprueba que se mantiene seguro bajo reintentos antes de escalar tráfico e ingresos.
FAQ
Las entregas duplicadas de webhooks son normales porque los proveedores optimizan por al menos una vez (at-least-once). Si tu endpoint hace timeout, devuelve un 5xx o cae la conexión brevemente, el proveedor reenviará el mismo evento hasta recibir una respuesta exitosa.
Usa el ID de evento único del proveedor (el identificador del evento del webhook), no la cantidad de la factura, la marca de tiempo o el correo del cliente. Almacena ese ID con una restricción única para que un reintento se detecte inmediatamente y se ignore de forma segura.
Inserta el registro del evento primero, antes de actualizar facturas, suscripciones o entitlements. Si la inserción falla porque el ID del evento ya existe, detén el procesamiento y devuelve éxito para que los reintentos no creen actualizaciones dobles.
Conserva los registros de deduplicación el tiempo suficiente para cubrir reintentos tardíos y para apoyar investigaciones. Un valor práctico por defecto es 30–90 días, y más tiempo (por ejemplo, 6–12 meses) si manejas disputas, contracargos o ciclos de suscripción largos; luego purga filas antiguas para mantener las consultas rápidas.
Verifica la firma antes de tocar los datos de facturación, luego parsea y valida los campos requeridos. Si la verificación de la firma falla, rechaza la petición y no escribas cambios de facturación, porque la deduplicación no te protegerá de eventos falsos que marquen "pagado".
Prefiere reconocer la recepción rápidamente después de que el evento esté almacenado de forma segura y mueve el trabajo pesado a procesos en background. Los handlers lentos provocan más timeouts, lo que causa más reintentos y aumenta la probabilidad de duplicados si algo no es totalmente idempotente.
Aplica solo cambios que avancen el estado y evita eventos obsoletos. Usa timestamps de eventos cuando estén disponibles y una prioridad simple de estados (por ejemplo, refunded no debe sobrescribirse por paid, y canceled no debe sobrescribirse por active).
No crees una nueva fila de entitlement por cada evento. Usa una regla tipo upsert como “asegurar un entitlement por usuario/producto/período (o por suscripción)”, luego actualiza fechas/límites y registra qué ID de evento causó el cambio para auditoría.
Escribe facturas, suscripciones y cambios de entitlements en una sola transacción de base de datos para que todo tenga éxito o falle junto. Esto evita estados partido como “factura pagada” pero “sin acceso concedido”, o “acceso revocado” sin un registro de reembolso correspondiente.
Sí. Crea un modelo WebhookEvents con un ID de evento único y un Business Process que compruebe “¿ya visto?” y salga temprano. Modela explícitamente invoices/subscriptions/entitlements en el Data Designer para que reintentos y replays no creen filas duplicadas.


