Webhooks
Los webhooks te notifican en tiempo real cuando algo cambia en un cobro, reembolso, contracargo o retiro — sin que tengas que hacer polling contra GET /v1/payments/{id}.
Configurar un endpoint
Los endpoints de webhooks se registran desde el panel de Paytami (dash.paytami.com), en la sección Webhooks de tu comercio:
- Indica una URL
https://en tu servidor que reciba peticionesPOST. - Elige a qué eventos suscribirte (o déjalo vacío para recibir todos — ver el catálogo completo).
- Guarda el secreto que se muestra — se ve una sola vez. Lo usas para verificar la firma de cada entrega (ver abajo).
Desde esa misma sección puedes ver el historial de entregas de cada endpoint, reintentar manualmente una entrega puntual, rotar el secreto o desactivar el endpoint.
Verificar la firma
Toda entrega lleva el header Paytami-Signature:
Paytami-Signature: t=1735689600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt: el timestamp Unix (segundos) en que se firmó.v1: el HMAC-SHA256 de"{t}.{cuerpo_crudo}", con tu secreto como llave.
Durante una rotación de secreto, el header puede traer dos valores v1= (uno con el secreto actual, otro con el anterior, mientras el período de gracia siga vigente) — basta con que alguno de los dos calce.
Verifica siempre contra el cuerpo crudo
El HMAC se calcula sobre los bytes exactos del cuerpo. Si tu framework ya parseó el JSON antes de que puedas acceder al texto original, la firma no va a coincidir aunque el contenido sea idéntico — asegúrate de leer el cuerpo crudo antes de cualquier middleware de parsing.
Ejemplo (Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyPaytamiSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('='))
)
// El header puede traer más de un "v1" durante una rotación de secreto —
// usa header.split(',').filter(p => p.startsWith('v1=')) si necesitas
// comprobar todos.
const timestamp = Number(parts.t)
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const received = Buffer.from(parts.v1)
const expectedBuf = Buffer.from(expected)
return received.length === expectedBuf.length && timingSafeEqual(received, expectedBuf)
}Rechaza cualquier entrega cuya firma no verifique, y cualquiera cuyo t esté fuera de una ventana de tolerancia razonable (protección contra replay de una petición capturada).
Reintentos
Si tu endpoint no responde 2xx, Paytami reintenta con backoff creciente: después de 1, 5, 30, 120, 300, 600 y 1440 minutos — hasta 8 intentos en total. Pasado el último intento, la entrega queda marcada como agotada y puedes reintentarla manualmente desde el panel.
- Responde
2xxtan pronto como hayas recibido el evento — procesa el efecto (activar acceso, enviar un correo, etc.) después, de forma asíncrona. Un endpoint lento arriesga que Paytami interprete la entrega como fallida por timeout. - Un timeout, error de DNS, TLS o conexión rechazada cuenta como intento fallido, igual que un
5xx.
Idempotencia en tu lado
Cada entrega trae el header Paytami-Event-Id con el id público del evento (evt_...). Un mismo evento puede llegar más de una vez —por un reintento, o por un reenvío manual desde el panel— así que tu handler debe ser idempotente: guarda los id de evento que ya procesaste y descarta los repetidos antes de aplicar su efecto de nuevo.

