Skip to content

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:

  1. Indica una URL https:// en tu servidor que reciba peticiones POST.
  2. Elige a qué eventos suscribirte (o déjalo vacío para recibir todos — ver el catálogo completo).
  3. 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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t: 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) ​

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 2xx tan 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.

Ver también ​

Documentación oficial de Paytami