Arquitectura de webhooks
Un webhook es una petición POST que la API envía al endpoint registrado cuando ocurre un evento suscrito. La entrega es asíncrona: la operación que origina el evento no espera a que el receptor responda.
Cada endpoint se registra con POST /v1/webhooks, que devuelve el secreto de firma. La suscripción admite entre 1 y 10 eventos por endpoint.
Firma de las entregas
El cuerpo de cada entrega se firma con HMAC-SHA256 usando el secreto del endpoint. La firma viaja en hexadecimal, prefijada por el algoritmo:
X-Webhook-Signature: sha256=<hex>La verificación consiste en calcular el HMAC-SHA256 del cuerpo recibido tal como llegó, sin reserializar el JSON, y compararlo con el valor de la cabecera. Una comparación en tiempo constante evita filtrar información por el tiempo de respuesta.
Firma con marca de tiempo
Cada entrega lleva además una segunda firma que incluye en lo firmado la hora del envío:
X-Webhook-Signature-Timestamped: t=<segundos>,v1=<hex>tes la hora del envío, en segundos Unix (UTC). Cada intento lleva la suya: un reintento no repite la marca del intento anterior.v1es el HMAC-SHA256, en hexadecimal, de la cadena<t>.<cuerpo>con el secreto del endpoint. El cuerpo es el recibido tal como llegó.
La cabecera X-Webhook-Signature se sigue enviando sin cambios: quien ya verifica con ella no tiene que modificar nada. La marca de tiempo de X-Veriko-Timestamp no está firmada; la de t sí, y es la que permite descartar una entrega vieja que un tercero vuelva a enviar.
Para verificarla:
- Separar la cabecera por comas y leer
tyv1. - Calcular el HMAC-SHA256 de
<t>.<cuerpo>con el secreto del endpoint. - Comparar el resultado con
v1en tiempo constante. - Rechazar la entrega si
tse aleja del reloj del receptor más de 5 minutos.
La ventana no sustituye a la deduplicación: el Delivery-Id sigue siendo estable entre reintentos, y conviene guardar los identificadores recibidos durante al menos 24 horas.
Cabeceras de una entrega
| cabecera | contenido |
|---|---|
X-Webhook-Signature | sha256= seguido del HMAC en hexadecimal |
X-Webhook-Signature-Timestamped | t= y la hora del envío en segundos Unix, una coma y v1= seguido del HMAC en hexadecimal de <t>.<cuerpo> |
X-Veriko-Event | Tipo de evento que originó la entrega, por ejemplo validation.completed |
X-Veriko-Delivery-Id | Identificador de la entrega, útil para deduplicar reintentos |
X-Veriko-Timestamp | Momento del envío, en ISO 8601 con sufijo Z |
Content-Type | application/json; charset=utf-8 |
La cabecera de firma es genérica y se configura independientemente del nombre del servicio. La de firma con marca de tiempo añade el sufijo -Timestamped a ese mismo nombre. Las otras tres se forman con el identificador del servicio. La diferencia de prefijos es deliberada.
El Delivery-Id se mantiene estable entre los reintentos de una misma entrega: es el valor que permite descartar duplicados en el receptor.
Cuerpo de una entrega de validación
El cuerpo es JSON y sigue la misma envoltura en los siete eventos de validación: validation.completed, validation.failed, validation.error, validation.returned y los tres validation.retry.*:
{
"event": "validation.completed",
"timestamp": "2026-07-21T05:00:05Z",
"data": {
"type": "validation",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"attributes": {
"validation_type": "direct",
"status": "valid",
"banxico_status": "valid",
"processing_time_ms": 1320,
"created_at": "2026-07-21T05:00:00Z",
"completed_at": "2026-07-21T05:00:05Z",
"client_ref": "orden-4812",
"banxico_confirmed": {
"amount": 1000.50,
"operationDate": "21-07-2026",
"processingTime": "13:45:02",
"trackingKey": "MBAN01002607211345ABCDEF",
"senderBank": "BBVA",
"receiverBank": "AZTECA",
"beneficiaryAccount": "••••5678"
}
},
"links": {
"self": "/v1/validations/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"cep_xml": "/v1/validations/3fa85f64-5717-4562-b3fc-2c963f66afa6/cep?format=xml",
"cep_pdf": "/v1/validations/3fa85f64-5717-4562-b3fc-2c963f66afa6/cep?format=pdf"
}
},
"meta": {
"datetime": { "timezone": "UTC", "format": "ISO 8601" }
}
}attributes.client_ref es la referencia que el integrador envió al crear la validación, devuelta tal cual. Sólo aparece cuando la petición la incluyó.
attributes.candidate_match (index desde 0 y account_last4) indica cuál de las cuentas_candidatas enviadas ganó. Sólo aparece cuando la petición llevó esa lista y una cuenta coincidió; la cuenta completa no viaja en el evento.
attributes.banxico_confirmed sólo aparece cuando Banxico confirmó el CEP — con banxico_status: valid, o returned cuando el comprobante se alcanzó a descargar antes del reverso—, y trae lo que Banxico determinó que en verdad se pagó: monto, fecha y hora de la operación, clave de rastreo, banco emisor y receptor, y la cuenta beneficiaria. La cuenta viaja enmascarada a los últimos 4 dígitos; el bloque nunca incluye nombres ni RFC.
Una devolución posterior: validation.returned
Un valid es el veredicto del momento de la consulta: el CEP acredita que la transferencia se liquidó, no que el dinero siguiera en la cuenta del beneficiario. Si Banxico reporta la devolución después, la validación pasa de valid a returned y se emite validation.returned. Eso ocurre en una revisión posterior, que se pide con POST /v1/validations/{id}/recheck o que el servicio hace solo durante las primeras 72 horas.
El evento lleva el mismo cuerpo que validation.completed —client_ref y banxico_confirmed incluidos— y añade attributes.payment_status, el estado del pago que delató la devolución:
{
"event": "validation.returned",
"timestamp": "2026-07-22T09:15:44Z",
"data": {
"type": "validation",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"attributes": {
"validation_type": "direct",
"status": "returned",
"banxico_status": "returned",
"client_ref": "orden-4812",
"payment_status": {
"code": "devuelto",
"label": "Devuelto",
"settled": false,
"reversed": true,
"checked_at": "2026-07-22T09:15:43Z"
}
}
}
}codeesdevueltooen_proceso_devolucion: los dos estados de Banxico que convierten la validación enreturned.reversedestrueen los dos casos, ysettledesfalse.checked_ates el instante de la consulta, en ISO 8601 UTC.received_atyprocessed_at, cuando Banxico los trae, son su hora local, tal cual.
Tres detalles que importan al integrar:
- El endpoint recibe sólo los eventos a los que se suscribió: para enterarse de una devolución posterior hay que suscribirlo a
validation.returned, además de avalidation.completed. - No se emite al crear la validación. Una operación que ya llega devuelta se entrega con
validation.completedystatus: returned;validation.returnedes la transición posterior. - Sale una sola vez por validación, aunque la revisión se repita. El CEP se conserva y sigue disponible en los enlaces del evento.
Una validación que sigue valid después de 72 horas no queda garantizada: el servicio deja de revisarla, pero una devolución más tardía sigue siendo posible. Para el control de un pago, la conciliación bancaria sigue siendo la segunda línea.
Tiempos de espera y reintentos
La conexión dispone de 5 segundos y la petición completa de 10 segundos. Agotado ese margen, la entrega se considera fallida.
Una entrega fallida se reintenta hasta 5 veces, con esperas crecientes:
| intento | espera desde el anterior |
|---|---|
| 1.º reintento | 1 minuto |
| 2.º reintento | 5 minutos |
| 3.º reintento | 30 minutos |
| 4.º reintento | 2 horas |
| 5.º reintento | 6 horas |
No todas las respuestas se reintentan. La distinción es si el fallo puede resolverse con el tiempo:
| respuesta del receptor | tratamiento |
|---|---|
2xx | Entrega correcta |
5xx, error de red o tiempo agotado | Se reintenta |
408 y 429 | Se reintentan: indican saturación temporal |
Resto de 4xx | No se reintenta. Un 400 o un 404 señalan un receptor mal configurado, y repetir la petición no lo corrige |
El cuerpo de la respuesta del receptor se conserva truncado a 2 048 bytes para el historial de entregas, consultable en GET /v1/webhooks/{id}/deliveries.
Las entregas salientes tienen dos topes por minuto hacia un mismo host destino: el de cada cuenta, fijado por su plan (de 10 en free a 300 en volumen), y un tope global de 600 que suman todas las cuentas. Cada cuenta cuenta por separado, de modo que varios clientes que apuntan al mismo host no se descuentan entre sí. Un receptor con muchos eventos simultáneos los recibe escalonados, no en ráfaga: una entrega que no cabe queda en retrying y se reintenta con el calendario de arriba, sin perderse. Los valores aplicables a la cuenta los devuelve GET /v1/usage/limits, y la escalera completa está en límites de tasa.
Desactivación automática
El endpoint acumula un contador de fallos consecutivos. Una entrega correcta lo devuelve a cero.
Al alcanzar 3 fallos consecutivos, el endpoint pasa a estado auto_disabled y deja de recibir entregas. El estado se consulta en GET /v1/webhooks junto con el contador consecutive_failures.
La reactivación es manual: se hace con PUT /v1/webhooks/{id} fijando el estado en active. El valor auto_disabled no puede asignarse desde la API — solo lo establece el sistema.
Estados de una entrega
| estado | significado |
|---|---|
pending | Encolada, todavía sin intentar |
retrying | Se pospuso por el límite de salida; Messenger la reintentará |
success | El receptor respondió 2xx |
failed | El último intento falló; Messenger puede programar otro si el fallo es recuperable |
Una entrega que estaba por enviarse (pending o retrying) cuando su validación se purgó con POST /v1/validations/{id}/purge/execute pasa a failed con error_message en validation_purged y no se envía. De las entregas de una validación purgada se conserva el registro, con su evento, estado, intentos y fechas, sin el cuerpo enviado ni la respuesta del receptor, y una validación purgada no emite eventos nuevos.
Recomendaciones para el receptor
- Responder
2xxen cuanto el cuerpo esté recibido y validado, y procesarlo después. Un receptor que procesa antes de responder consume el margen de 10 segundos y provoca reintentos innecesarios. - Deduplicar por
X-Veriko-Delivery-Id: un reintento repite la misma entrega. - Verificar la firma antes de leer el contenido. Con
X-Webhook-Signature-Timestampedse puede además rechazar una entrega cuyatse aleje más de 5 minutos del reloj del receptor, sin rechazar los reintentos. - Antes de liberar mercancía o servicio por un
validation.completedcon veredictovalid, comparar el monto y la cuenta beneficiaria debanxico_confirmedcontra los del pedido — ver la sección anterior. - Tratar un
validcomo reversible durante sus primeras 72 horas y suscribir el endpoint avalidation.returnedpara enterarse de una devolución posterior.