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.
Lo que responde tu endpoint puede continuar la conversación
Cuando el AI employee de tipo JSON despacha a tu URL —o una acciónsend_json, que te manda una petición fija sin que la redacte un
modelo—, lo que tu servidor conteste puede ser leído por otro empleado AI para seguirle
escribiendo al cliente, si el equipo del negocio configuró ese procesamiento: en el empleado
JSON desde su panel, o en la propia acción. No es automático: viene apagado. Una vez configurado
en el empleado JSON, no importa cómo se lanzó: desde el panel, como acción de otro empleado
AI, dentro de una campaña o desde esta API (run-ai,
ai-assistance).
Esto no cambia el contrato de esta API: mismos shapes, mismos status, mismos códigos de
error, y tu respuesta se sigue registrando igual que hoy. Lo único distinto es qué hacemos
nosotros con ella. Aun así, hay cinco cosas que conviene saber antes de decidir qué devuelves:
-
El cuerpo de tu respuesta se le muestra a un modelo. Contesta solo con lo que te parezca
bien que el cliente llegue a saber: ni credenciales, ni datos de otro cliente, ni volcados
internos. Si es largo, se recorta. Antes de mostrárselo tapamos los valores de los campos con
nombre de credencial (
token,password,api_key…), los números de 18 dígitos —aunque vengan partidos por espacios o guiones— y la parte/pay/…de los enlaces, así que si tu respuesta trae una CLABE o un enlace con/pay/…, el AI no podrá pasárselos al cliente. Ese filtro es una red y no una garantía: no reconoce todos los nombres de campo ni los enlaces de otras pasarelas, así que no mandes credenciales ni datos bancarios confiando en él. -
Solo se lee si respondes
2xxy con cuerpo, y ni así es seguro. Un error o una respuesta vacía no continúan nada, y ese despacho no se repite si respondes4xxo5xx. Solo se repite una vez si no respondes en 30 segundos o falla la conexión, así que puede llegarte dos veces y sin clave de idempotencia: este despacho no llevawebhook-idni firma, así que la deduplicación corre por tu cuenta. Los reintentos, el timeout y elwebhook-idde Entrega y reintentos son solo de los webhooks de eventos de pago. Sí se continúa aunque en ese momento la conversación la atienda una persona o esté pendiente. No hay un plazo garantizado entre tu respuesta y el mensaje al cliente. -
La cadena tiene un tope, y depende de dónde salió el despacho. Si la continuación vuelve a
despachar a tu URL, esa respuesta también puede continuar la conversación, hasta el tope:
- Hasta dos continuaciones cuando el despacho sale de lo que abre el turno: el empleado JSON
que corre por sí mismo —el asignado, o uno lanzado directamente desde el panel, una campaña o
esta API—, una acción
send_jsonsuelta en un grupo de acciones, o el action set del empleado que responde un mensaje del cliente, un mensaje programado o el botón «Ejecutar» sobre el asignado (con unsend_jsonsuyo o un empleado JSON que ese set lanza). - Una sola cuando despacha el action set de un empleado que otro lanzó: otro action set,
esta API (
run_ai,ai_assistance,assign_ai_employeeconrun: trueo Smart Actions), una campaña, o «Ejecutar» sobre un empleado que no es el asignado. También se queda en una el empleado JSON que ese set lance.
- Hasta dos continuaciones cuando el despacho sale de lo que abre el turno: el empleado JSON
que corre por sí mismo —el asignado, o uno lanzado directamente desde el panel, una campaña o
esta API—, una acción
-
Una campaña te despacha una vez por destinatario. Si el equipo del negocio pone un
send_jsono un empleado JSON en una campaña, te llega una petición por destinatario, y con el procesamiento configurado cada respuesta puede continuar su conversación. Unsend_jsonque va antes de la primera acción de AI de la campaña sale al ritmo del envío. Si va después, sale desde una cola que acota cuántos arrancan a la vez por negocio, pero no cuántos siguen corriendo: uno que tarda puede liberar su lugar antes de terminar. No hay un ritmo garantizado. Dimensiona tu endpoint por el tamaño de la audiencia, no por el de una conversación. -
No aplica a los despachos que llevan el campo del pago, aunque no traiga ningún pago (un
arreglo vacío, o un objeto con
datavacío). Ahí la respuesta no se lee, por diseño: un endpoint que hace eco del request devolvería datos bancarios, y el corte no distingue un campo sin pagos de uno con pagos.
Disponibilidad: se está habilitando por etapas a nivel de la plataforma, no cuenta por
cuenta.
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
Id único de la entrega. Estable entre reintentos de un mismo evento —
úsalo para deduplicar (ver Entrega).
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.
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.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.
5
Un aviso puede llegar tarde
Si una entrega no se generó por una falla de nuestro lado, podemos recuperarla,
normalmente en unos 15 minutos (hasta 3 días después del evento). El aviso
recuperado trae en
event_at la hora original del evento, no la de envío: usa
event_at para fechar el pago, nunca la hora en que recibes el webhook. Al
reactivar una suscripción pausada o deshabilitada, o al agregarle un evento,
puedes recibir de golpe avisos de pagos de hasta 3 días atrás. El objeto
payment trae el estado del pago al enviarlo, no el del evento: un
payment.registered recuperado puede llegar con status: "credited" o
"failed". No es un cobro nuevo: como en «No asumas orden», usa
data.payment.uuid + status como la verdad.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.