POST firmado a la URL que registres, con el detalle del pago.
Estos webhooks son salientes (1to1 → tu servidor). No los llamas tú: los
recibes. Cada entrega va firmada (estándar Standard Webhooks)
para que verifiques que vino de nosotros y no fue alterada.
Configurar un endpoint
Desde el dashboard, en Configuración → API y Conexiones, agrega la URL de tu sistema y elige los eventos que quieres recibir. Al crearlo obtienes un signing secret (whsec_…) que se muestra una sola vez — guárdalo como un
secreto: es la llave con la que verificas cada webhook. Puedes registrar hasta
10 endpoints por negocio.
Catálogo de eventos
payment.failed cubre solo los pagos manuales marcados como fallidos por tu
equipo. Los fallos de pagos online (tarjeta declinada, sesión expirada) no
emiten webhook.El payload
CadaPOST lleva un cuerpo JSON con esta forma. El ejemplo es un pago manual
acreditado:
event_at es el momento en que ocurrió el evento del pago (para payment.failed
es la hora de la falla). Es distinto del header webhook-timestamp, que es el
instante de envío usado para la firma anti-replay.provider: "manual"):
bank_account (la etiqueta de la cuenta a la que entró el dinero, p. ej.
"BANORTE0876"), bank_datetime (fecha/hora en banca, opcional) y files
(comprobantes subidos). En pagos online quedan null / []. En pagos
manuales, transaction_id es el ID de banca capturado por el operador al
acreditar (null si no se capturó); en pagos online es el id del
proveedor. En todo pago online acreditado receipt_url trae siempre una
URL de nuestro dominio (https://…/pay/receipt/<uuid>), nunca el recibo
hospedado del proveedor; en pagos manuales es null. Esa página no requiere
autenticación y no tiene vencimiento por tiempo (a diferencia de los links del
proveedor); deja de servir si el pago se borra o deja de estar acreditado. El
PDF del recibo se descarga agregando el sufijo /pdf a esa misma URL.
El payload crece con el tiempo: podemos agregar campos nuevos al webhook sin aviso
previo, y eso no cuenta como cambio incompatible. Los que ya están documentados aquí no
los quitamos ni los renombramos sin anunciarlo antes, y un cambio incompatible llega como
un tipo de evento nuevo, nunca alterando uno existente.Por eso tu parser debe ignorar lo que no reconozca —tanto claves como tipos de
evento— en vez de fallar: no configures
additionalProperties: false en tu validación de
schema, ni uses structs que rechacen campos desconocidos. Una entrega que tu endpoint
rechaza se reintenta durante ~3 días y puede terminar deshabilitando la suscripción.Esto aplica al shape del payload. Los límites de tamaño y el contenido de los campos
de texto libre pueden cambiar: si dimensionaste tu almacenamiento por los topes de arriba,
revisa esta guía antes de asumir que siguen siendo los mismos.description y custom_fields: el contexto que escribe tu equipo
Dos campos que describen de qué se trata el pago, y que salen de lo que tu propio
negocio captura:
descriptiones la nota del pago. La escribe quien lo registra desde el panel, o la redacta tu AI employee si le configuraste una guía para eso. Es prosa libre:nullcuando nadie escribió nada.custom_fieldsson los campos personalizados que definiste en tu cuenta —«Tipo de pago», «Pedido», los que necesites— con el valor que se capturó en ese pago. Mientras el campo esté en despliegue por etapas la clave puede no venir; cuando viene es siempre un objeto:{}si no se capturó ninguno.
custom_fields son los nombres que tú les pusiste, tal cual, así que si
renombras un campo en tu cuenta la clave cambia en los pagos siguientes. Si vas a
mapearlos a tu sistema, hazlo por el nombre que uses hoy y revisa ese mapeo cuando
renombres.
Cuando las dos claves vienen, hoy solo las pobla el carril de pagos manuales: en los pagos
con tarjeta o link llegan null y {}.
Disponibilidad: estos dos campos se están habilitando por etapas a nivel de la
plataforma, no cuenta por cuenta. Si todavía no los ves en tus entregas es que todavía no
están publicados — escríbenos y te avisamos cuando lo estén.
Si recibes eventos por dos vías, deduplica por payment.uuid
Además de estos webhooks, un AI employee de tipo JSON puede despachar el pago a una URL
tuya como parte de lo que extrae de la conversación. Si usas las dos vías, el mismo pago
te va a llegar dos veces, con el mismo objeto payment en ambas.
Deduplica por data.payment.uuid, que es estable entre las dos. No uses el folio: los
pagos con tarjeta y link no siempre lo traen.
Si además usas un AI employee de tipo JSON que adjunte el pago, lo que recibes dentro de su campo es este mismo objeto data completo —payment, contact_info y conversation_info—, así que el código que ya escribiste para procesarlo te sirve igual. Lo único que no lleva es el envelope (id, type, event_at), porque ése describe esta entrega en particular.
Ese campo tiene dos formas, y el AI employee elige cuál según lo que escriba en él.
- Si no escribe nada ahí, o escribe un texto, recibes el arreglo directo:
"pago": [ { "payment": …, "contact_info": …, "conversation_info": … } ] - Si escribe un objeto con su propia lectura, el paquete se anida bajo
data:"pago": { "tipo": "anticipo", "data": [ { "payment": … } ] }
data. Un parser escrito solo contra la primera forma deja de encontrar el pago el
día que el AI escriba un objeto ahí.Esa clave data no es el objeto data del webhook: es el arreglo de esos objetos. Y la
misma regla aplica a los campos de archivos.Disponibilidad: esta vía se está habilitando por etapas a nivel de la plataforma, no
cuenta por cuenta. Escríbenos si quieres que te avisemos cuando esté publicada.
fail_reason: texto libre escrito por una persona
En payment.failed, fail_reason trae el motivo que tu equipo escribió a mano
al marcar el pago como fallido — por qué no pudieron verificarlo. Es prosa en el
idioma de quien lo escribió, no un código de una lista cerrada: no lo uses como
llave de una tabla de traducciones ni intentes parsearlo. El panel lo exige antes
de dejar marcar el pago, pero haz null-check igual: el endpoint todavía lo
acepta vacío mientras dura el despliegue de esta función. En cualquier otro
evento es null.
Como payment.failed cubre solo pagos manuales (los fallos de pasarela no
emiten webhook), nunca vas a recibir un código de proveedor por este campo.
Headers de cada entrega
string
Id único de la entrega. Estable entre reintentos de un mismo evento —
úsalo para deduplicar (ver Entrega).
integer
Instante de envío (segundos Unix). Se recomputa en cada reintento y entra en la
firma. Rechaza los que estén fuera de una ventana razonable (±5 min) para
protegerte de replays.
string
Una o más firmas
v1,<base64> separadas por espacio (habrá dos durante una
rotación de secret). La entrega es válida si alguna coincide.string
El tipo de evento (
payment.credited, payment.failed, …). Coincide con
type del cuerpo.string
Siempre
1to1-Webhooks/1.0.Verificar la firma
La firma es un HMAC-SHA256 del contenido{webhook-id}.{webhook-timestamp}.{body},
donde body es el cuerpo crudo exacto que recibiste (no lo re-serialices).
La clave es tu whsec_… con el prefijo removido y el resto decodificado de base64.
La forma más simple es la librería oficial de Standard Webhooks, que maneja la
rotación y la comparación en tiempo constante por ti:
Entrega y reintentos
1
Responde 2xx rápido
Contesta con cualquier
2xx en cuanto recibas el webhook. Si tardas más de
10 segundos o respondes otro código, lo tratamos como fallo y
reintentamos.2
Deduplica por webhook-id
La entrega es at-least-once: un mismo evento puede llegar más de una vez
(un reintento tras un timeout, por ejemplo). El
webhook-id es estable entre
reintentos — guárdalo y descarta los repetidos.3
No asumas orden
Los eventos no llegan garantizadamente en orden. Usa
data.payment.uuidstatuscomo la verdad, no el orden de llegada: unpayment.registeredque llegue tarde no debe pisar unpayment.creditedque ya procesaste.
4
Reintentos ~3 días
Un endpoint caído recibe reintentos con espaciado creciente durante ~3 días.
Si sigue fallando, la suscripción se deshabilita automáticamente (tras 5
fallos consecutivos agotados) — la reactivas desde el dashboard.
Rotar el secret
Desde el dashboard puedes rotar el signing secret cuando quieras. Durante una gracia de 24 horas, cada webhook se firma con el secret nuevo y el anterior a la vez (dos firmas en el header). Así actualizas tu sistema sin perder ni rechazar eventos: valida contra cualquiera de las dos; cuando vence la gracia de 24 h, el secret anterior deja de firmarse automáticamente.Comprobantes
En un pago manual,files[] lista los comprobantes subidos con referencias
estables — uuid, name y mime_type — pero sin URL de descarga: el
payload es un snapshot que se reintenta durante días y una URL firmada expiraría
en el camino. Para descargar el archivo, pide un enlace firmado con la misma API
key de tu integración — no requiere configuración nueva. El {paymentUuid} del
path es data.payment.uuid y el {fileUuid} es el uuid del comprobante
dentro de data.payment.files[], ambos tomados del propio evento:
download_url vence a los 30 minutos (expires_at); si expira antes de
que lo uses, repite el mismo GET para obtener uno nuevo.
El endpoint responde 404 en dos casos, ambos terminales — ver el detalle en el
catálogo de errores: PAYMENT_NOT_FOUND si el paymentUuid no
resuelve para tu API key, y FILE_NOT_FOUND si el fileUuid no es un
comprobante confirmado de ese pago.
Próximos pasos
Autenticación
Cómo se autentica tu integración con la API key.
Errores
El contrato de errores de la API pública.