Webhooks firmados para pagos cripto: verifica antes de entregar nada
Puntos clave
- Tu endpoint de webhooks es una URL pública que mueve dinero. Cualquiera en internet puede hacerle POST, así que verificar la firma no es un detalle: es la autenticación.
- Primero verifica, después parsea, al final actúa. Recalcula el HMAC sobre el cuerpo crudo con comparación en tiempo constante y descarta lo que no cuadre antes de que tu código de pedidos vea el payload.
- La idempotencia pesa más en cripto que en tarjetas, porque no hay contracargo que deshaga un pedido entregado dos veces. Llave cada efecto secundario al ID del cargo o del evento.
- Payzum trae webhooks firmados, secretos encriptados, 2FA y audit log completo, más un integration playground para ensayar confirmaciones, expiraciones y sobrepagos antes de mover una sola moneda.
Por qué el endpoint de pagos es la ruta más riesgosa de tu app
Casi todo tu backend está protegido por una sesión, un token o una red privada. Tu callback de pagos no tiene nada de eso: es una URL que publicas, que acepta POST no solicitados desde internet abierta, y cuyo trabajo es avisarle a tu sistema que entró dinero.
Esa combinación es rara, y los equipos la subestiman todo el tiempo. Los tres fallos que vemos una y otra vez:
- Confiar sin autenticar. El handler parsea el JSON, lee
status: "confirmed"y marca el pedido como pagado. Cualquiera que adivine la URL y la forma del payload se lleva mercadería gratis. Una allowlist de IPs ayuda un poco, pero no reemplaza la prueba de que este proveedor generó exactamente este payload. - Entregar desde el navegador. El cliente cae en
/gracias?status=oky el frontend llama a un endpoint interno que cambia el estado del pedido. Esa es una URL que controla el cliente. Los redirects son experiencia de usuario; los webhooks son la verdad. - Asumir entrega exactly-once. Los webhooks se reintentan. Una respuesta lenta, un deploy a mitad del request, un 502 de tu balanceador — y el mismo evento llega dos veces. Si tu handler no es idempotente, despachas el pedido dos veces o acreditas el saldo dos veces.
En cripto hay dos más que el mundo de tarjetas nunca tuvo que pensar. Primero, el evento que importa es una confirmación on-chain, no una autorización: no hay un evento "liquidado" posterior que corrija discretamente uno optimista. Segundo, el monto no está garantizado. El cliente paga desde su propia wallet y puede mandar un poco de menos, un poco de más, o a tiempo pero después de que tu factura expiró.
Lo que cuesta un handler descuidado cuando el pago es final
En tarjetas, un handler malo es caro pero sobrevivible. Entregas contra un callback falso y todavía tenés palancas: el cargo nunca se capturó, o lo reembolsás, o el adquirente lo revierte. La reversibilidad que hace incómodo el mundo de tarjetas también perdona, en silencio, mucho pecado de integración.
Los pagos on-chain quitan esa red en las dos direcciones. Nadie puede quitarte tu facturación legítima — y nadie puede recuperar la mercadería que despachaste contra un evento que no verificaste. En concreto:
- Un evento "pagado" falsificado es pérdida definitiva. Si un atacante puede postear una confirmación falsa, se lleva la descarga, la licencia, el upgrade de cuenta o el envío físico, y no hay pago que revertir porque nunca hubo pago.
- Un evento duplicado también es pérdida definitiva. Dos entregas de la misma confirmación, dos acreditaciones al saldo del cliente, un solo pago recibido. Con volumen, una tormenta de reintentos durante una caída convierte un incidente de cinco minutos en un proyecto de reconciliación.
- Un evento perdido es cola de soporte. El cliente pagó, la red confirmó, tu endpoint estuvo caído seis minutos y ahora un cliente que ya pagó mira un "esperando pago". Cada uno de esos es un ticket, y los que no escriben simplemente se van.
- Los desajustes de monto terminan en planilla. Sin estados explícitos para pago corto, sobrepago y expiración, cada "mandó 49,97 en vez de 50,00" cae en manos de una persona. La versión completa de ese problema la contamos en la guía de pasarela de pagos cripto con API REST.
Nada de esto es ingeniería exótica: son unas cuarenta líneas de handler más una decisión de arquitectura — no confíes en nada que no hayas verificado. Pero hay que construirlo antes del primer pago real, no después del primer incidente.
Por qué los hábitos de webhooks de tarjetas no sirven en cripto
El ciclo de vida de las tarjetas entrenó a una generación de backends para tratar los webhooks como chismes de estado. Una autorización es provisional. La captura puede llegar minutos o días después. La liquidación es un lote. Una disputa puede reabrir la venta hasta unos 120 días más tarde. En ese mundo el webhook es una señal más, y el libro de la verdad es el panel del procesador.
En un flujo cripto sin custodia el modelo se invierte. Hay un solo momento que importa: el pago confirma on-chain y los fondos están en la wallet del comercio. No hay captura, no hay lote de liquidación, no hay corrida de payout y no hay ventana de disputa. El webhook no es un chisme sobre un estado que después se revisará: es el aviso de que una transferencia final e irreversible ya ocurrió.
Eso vuelve al webhook, a la vez, más simple y más crítico. Más simple, porque modelás menos estados. Más crítico, porque tu handler es lo único que separa un evento real on-chain de tu lógica de entrega — y también lo único que separa un request HTTP falsificado de esa misma lógica.
Las pasarelas cripto custodiales enturbian todavía más el asunto. Su webhook dice "pagado", pero el dinero está en su balance: seguís esperando un calendario de payout, un umbral o una revisión de riesgo. El evento y el dinero volvieron a desacoplarse. Esa es la diferencia estructural que elimina un procesador de pagos cripto sin custodia: cuando el webhook dispara, la liquidación ya sucedió, hacia una dirección cuyas llaves tenés vos.
Cómo lo resuelven los webhooks firmados de Payzum
Payzum es un procesador de pagos sin custodia y solo cripto. La superficie para developers es deliberadamente aburrida — una API REST con API keys y webhooks firmados — y las partes que suelen salir mal en una integración cripto son comportamiento de plataforma, no tarea tuya.
Cada evento lleva firma criptográfica
Cuando un pago confirma on-chain, Payzum llama a tu endpoint con un webhook firmado: una firma calculada sobre el payload con tu secreto de webhook. Tu handler la recalcula y la compara antes de creerle a nada. Es la construcción de hash con clave descrita en RFC 2104 — la misma primitiva detrás de los esquemas de firma que usa toda la infraestructura de pagos, y conceptualmente el mismo problema que el IETF estandarizó de forma más general en RFC 9421, HTTP Message Signatures.
La consecuencia práctica: un request sin firma válida sobre los bytes exactos que recibiste no puede venir de Payzum, y tu código de pedidos nunca tiene que mirarlo.
Los secretos se tratan como secretos
Los secretos de webhook y las API keys viven encriptados en el panel, la cuenta soporta 2FA y cada acción queda en un audit log completo. Cuando rotás un secreto o alguien cambia un endpoint hay registro de quién y cuándo — la diferencia entre un incidente que podés explicar y uno que solo podés adivinar.
Eventos sin ambigüedad, porque el cargo no la tiene
Los cargos y las facturas son objetos de primera clase: monto exacto, tu propia referencia, ventana de expiración y detección de sobrepago. Los casos borde incómodos de cripto dejan de ser aritmética abierta dentro de tu handler y pasan a ser estados definidos sobre los que ramificar. Tu lógica reacciona a "este cargo, este estado"; no tiene que deducir intención a partir de un monto transferido.
Un playground antes de mover dinero real
Podés ensayar el ciclo completo en el integration playground: crear cargos, disparar confirmaciones, recibir y verificar webhooks firmados de verdad, y ejercitar los caminos de expiración y sobrepago. Es decir: tu flujo de pagos puede quedar cubierto por tests como cualquier otra parte del sistema — incluyendo los caminos de falla, que acá son los que importan.
Y el evento significa que el dinero ya es tuyo
El punto arquitectónico que vale repetir: Payzum nunca retiene los fondos. La liquidación va on-chain, directo a una wallet que vos controlás, en segundos — ~0,4s en Solana, ~2s en Base y Polygon. Con auto-conversión activada, pague con lo que pague el cliente, liquida como USDC o USDT. Cuando tu handler marca el pedido como pagado no queda ningún estado de payout esperando.
Cómo manejar un webhook de pago firmado, paso a paso
Este es el checklist que recorremos con los equipos de ingeniería. Es corto a propósito: la disciplina está en el orden, no en la cantidad de código.
- Capturá el cuerpo crudo antes de que algo lo parsee. Las firmas se calculan sobre bytes exactos. Si tu framework vuelve a serializar el JSON antes de que lo veas, cambia el orden de claves o los espacios y toda firma falla. En la mayoría de los stacks esto significa configurar un parser de raw body solo en la ruta del webhook.
- Verificá la firma con comparación en tiempo constante. Recalculá el HMAC sobre el cuerpo crudo con tu secreto y compará con una función timing-safe (
crypto.timingSafeEqualen Node,hmac.compare_digesten Python). Un===filtra información que un atacante paciente aprovecha. Si falla, devolvé 401 y cortá — y no loguees el payload como si fuera real. - Deduplicá antes de actuar. Tomá el identificador del evento o del cargo y escribilo en una tabla con constraint único, dentro de la misma transacción que tu efecto secundario. Si el insert choca, ya procesaste ese evento: devolvé 200 y no hagas nada. Eso es lo que vuelve inofensivos los reintentos.
- Respondé rápido, trabajá asincrónicamente. Hacé la verificación y la deduplicación inline, después encolá el trabajo pesado — emails, entrega, facturación — y devolvé 2xx enseguida. Un handler que llama a tres servicios internos antes de responder es un handler que da timeout, se reintenta y crea los duplicados contra los que acabás de diseñar.
- Reconciliá contra el cargo, no contra el relato. Antes de entregar, confirmá que el estado y el monto del cargo coinciden con el pedido que esperás. La firma prueba que el mensaje es auténtico; tu chequeo prueba que es el mensaje de este pedido, en el estado que exigís.
- Dejá una red de respaldo. Los endpoints se caen. Tené un job programado que busque pedidos trabados en "esperando pago" más allá de una ventana razonable y los reconcilie contra la API. Los webhooks son el camino rápido, no el único camino.
- Ensayá en el playground y recién ahí, producción. Corré el ciclo completo — confirmación, expiración, sobrepago, entrega duplicada — antes de tu primera transacción real. Los nombres exactos de headers, los esquemas de payload y los tipos de evento están en la documentación de la API.
Conceptualmente, el handler tiene esta forma:
# 1) Tu backend creó el cargo antes (auth con API key)
POST /charges → { amount, asset, reference: "pedido_1842", expires_in }
← { charge_id, payment_details, status: "pending" }
# 2) El cliente paga desde su wallet; la red confirma en segundos
# 3) Payzum te llama de vuelta — firmado
POST https://tuapp.com/webhooks/payzum
X-Signature: <HMAC sobre el cuerpo crudo, con tu secreto de webhook>
{ "charge_id": "...", "status": "confirmed", "reference": "pedido_1842" }
# 4) Tu handler, en este orden:
raw = leer_cuerpo_crudo(request)
esperada = hmac_sha256(secreto_webhook, raw)
if not comparacion_tiempo_constante(esperada, firma_header):
return 401 # nunca llegó a tu código de pedidos
evento = parse(raw)
if ya_procesado(evento.id): # constraint único sobre el id del evento
return 200 # reintento — no-op seguro
pedido = cargar_pedido(evento.reference)
if pedido.monto != evento.monto or evento.status != "confirmed":
marcar_para_revision(pedido); return 200
marcar_pagado(pedido); encolar(entregar, pedido)
return 200
# Los fondos YA están en tu wallet — no existe paso de payout.
La última línea del paso 4 es la que debería resultar rara si venís de una pasarela de tarjetas: no hay estado "esperando payout" en ningún punto del ciclo. La liquidación es el pago.
Qué construyen los equipos sobre webhooks de pago firmados
Cinco patrones que vemos repetidamente, todos sobre la misma superficie de eventos:
- Marketplace con entrega por pedido. Un marketplace regional crea un cargo por pedido y llavea los webhooks a su propia referencia. La confirmación verificada libera el ítem a la cola de fulfillment del vendedor. Como el pago es final, no queda una ventana de disputa de 120 días colgando sobre mercadería ya entregada — ni un adquirente reteniendo el saldo mientras el comprador espera.
- Entrega digital instantánea. Una plataforma de cursos o una empresa de software emite la licencia, desbloquea la descarga o provisiona la cuenta en cuanto el webhook firmado verifica. En redes rápidas eso son segundos después del pago — y el mismo evento verificado que da acceso es contra el que concilia finanzas. La otra mitad del flujo está en añadir pagos cripto a tu tienda online.
- Alta y renovación de suscripciones. Un SaaS maneja su propia UI de facturación y usa los webhooks para extender la ventana de acceso en cada cobro recurrente. Sin reversiones involuntarias a mitad de ciclo no hay tickets de "me cortaron el acceso porque entró una disputa" — lo desarrollamos en suscripciones en cripto sin contracargos.
- Conciliación de POS y multi-sucursal. Un operador con cobro por QR en varios locales alimenta su back office con los eventos de pago verificados, llaveados por terminal y por cajero, para que el cierre diario cuadre contra el registro on-chain en vez de contra un extracto bancario que llega dos días tarde.
- APIs que cobran a agentes. Si tu producto es una API, la misma cuenta puede publicar un endpoint x402 delante: configurás tu endpoint actual, tu API key y un precio en el panel, Payzum devuelve el 402, el pago se liquida a través de un facilitator externo y la llamada pagada se proxea a tu endpoint real. Los agentes de IA pagan USDC en Base por llamada, directo a tu wallet, sin que implementes protocolo alguno.
Callbacks de tarjetas vs webhooks firmados para pagos cripto
| Dimensión | Callbacks de tarjeta / pasarela custodial | Webhooks firmados de Payzum |
|---|---|---|
| Qué significa el evento | Estado provisional en un ciclo de varios pasos (auth → captura → liquidación) | Un pago on-chain ya confirmado y liquidado |
| Autenticación | La firma varía según el proveedor; algunos solo usan allowlist de IPs | Firma criptográfica sobre el payload, verificada con tu secreto |
| Dónde está el dinero al dispararse | En el balance del proveedor, esperando una corrida de payout | En tu propia wallet — la liquidación ya ocurrió |
| ¿Se puede revertir después? | Sí — reembolsos, reversiones y disputas hasta ~120 días | No. Finalidad on-chain; no existe ventana de contracargo |
| Entregas duplicadas | Posibles — tu handler debe ser idempotente | Posibles — tu handler debe ser idempotente (misma disciplina, más en juego) |
| Casos borde de monto | Raros en tarjetas; improvisados en casi toda pasarela cripto | Expiración de factura y detección de sobrepago como estados de plataforma |
| Probar los caminos de falla | La calidad del sandbox varía | Integration playground para el ciclo completo, firmas incluidas |
| Secretos y trazabilidad | Varía | Secretos encriptados, 2FA y audit log completo de cada cambio |
Objeciones frecuentes de developers — resueltas
"¿No puedo hacer polling a la API en vez de exponer un endpoint?"
Podés, y con poco volumen funciona. Pero el polling cambia una superficie de seguridad por una de latencia y costo: o consultás poco y hacés esperar a clientes que ya pagaron, o consultás agresivamente y quemás requests sobre pedidos que no se movieron. La respuesta habitual es ambas cosas: webhooks como camino rápido y un job de reconciliación como respaldo para lo que quede trabado. Es el paso 6 del checklist de arriba, y es lo que convierte una caída del endpoint en una demora en vez de un incidente.
"¿No alcanza con una allowlist de IPs o mTLS?"
Los controles de red responden "de dónde vino esto"; la firma responde "quién produjo exactamente este payload y si fue modificado". Son preguntas distintas, y solo la segunda sobrevive a un proxy, un CDN, una IP de salida compartida o un cambio de infraestructura del lado del proveedor. Usá controles de red como defensa en profundidad si querés, pero la firma es el chequeo que no se puede saltear.
"Si el pago es final, ¿cómo hago un reembolso?"
La finalidad elimina solo las reversiones involuntarias. La atención al cliente sigue siendo tuya: cuando corresponde un reembolso según tu política, devolvés fondos desde tu propia wallet. Te quedás con la decisión y perdés la cuota de disputas, los cargos de arbitraje y el fraude de "no me llegó" sobre mercadería que sí entregaste.
"¿Tengo que correr infraestructura blockchain para saber que un pago confirmó?"
No. Payzum se encarga de la gestión de direcciones, la detección de pagos y el seguimiento de confirmaciones en Bitcoin, Ethereum, Solana, Polygon, Base, Arbitrum, Optimism, BNB Chain y Avalanche. Tu lado de la integración es HTTPS puro: crear cargos con una API key y verificar firmas con tu secreto de webhook. Sin nodos, sin proveedores de RPC y sin llaves privadas en tus servidores de aplicación.
"¿Cuánto cuesta por transacción?"
La liquidación paga comisiones de red on-chain — centavos en Base, Polygon y Solana — en vez de un porcentaje de cada venta. // confirmar pricing actual — agenda una llamada para el pricing vigente según tu volumen esperado.
Preguntas frecuentes
¿Qué son los webhooks firmados para pagos cripto?
Son callbacks HTTP que un procesador de pagos envía a tu servidor cuando ocurre un evento de pago, con una firma criptográfica calculada sobre el payload usando un secreto que solo conocen el procesador y vos. Tu endpoint recalcula la firma y la compara antes de creerle al evento, así un request falsificado o modificado nunca puede marcar un pedido como pagado.
¿Cómo verifico la firma de un webhook de Payzum?
Leé el cuerpo crudo del request antes de cualquier parseo, recalculá el HMAC sobre esos bytes exactos con tu secreto de webhook y compará contra el header de firma usando una función timing-safe como crypto.timingSafeEqual en Node o hmac.compare_digest en Python. Descartá lo que no coincida. Los nombres exactos de headers y los esquemas de payload están en la documentación de la API de Payzum, y podés ensayar todo el flujo en el integration playground.
¿Por qué me falla siempre la verificación de firma?
Casi siempre porque el cuerpo se volvió a serializar antes de hashearlo. Los frameworks que parsean JSON automáticamente pueden cambiar el orden de las claves, los espacios o la codificación, y la firma cubre bytes exactos. Configurá un parser de raw body específicamente en la ruta del webhook, hasheá eso y verificá antes de parsear.
¿Tengo que manejar entregas duplicadas de webhooks?
Sí. Cualquier transporte de webhooks reintenta tras timeouts o errores, así que asumí entrega at-least-once. Guardá el identificador del evento o del cargo con un constraint único en la misma transacción que tu efecto secundario: si el insert choca, el evento ya se procesó y tu handler devuelve 200 sin hacer nada. Con pagos on-chain finales no hay contracargo que deshaga una entrega duplicada, así que la idempotencia no es opcional.
¿Qué pasa si mi endpoint está caído cuando el pago confirma?
El pago se liquida igual: es on-chain y sin custodia, así que los fondos llegan a tu wallet responda o no tu servidor. El estado de la aplicación lo recuperás reconciliando: un job programado que revise los pedidos trabados en estado impago contra la API y los actualice. Los webhooks son el camino rápido, no la única fuente de verdad.
¿Puedo probar webhooks firmados sin mover fondos reales?
Sí. El integration playground te deja crear cargos, disparar confirmaciones y recibir webhooks firmados de verdad de punta a punta, incluidos los caminos de expiración y sobrepago, antes de tu primera transacción en vivo — así tu flujo de pagos queda cubierto por tests automatizados como cualquier otra parte del sistema.
Revisemos tu integración de webhooks
Cada modelo de pedidos es distinto: qué entregás, cuándo considerás final un pago, cómo conciliás, qué historia de reintentos tenés. Agenda 20 minutos con nuestro equipo y mapeamos tu flujo exacto: creación de cargos, verificación de firma, idempotencia, los estados que vale la pena manejar y liquidación sin custodia a tu propia wallet. Sin pitch — un plan técnico concreto para tu stack.
¿Preferís no usar el embed? Agenda directamente acá · [email protected]