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>
  • t es la hora del envío, en segundos Unix (UTC). Cada intento lleva la suya: un reintento no repite la marca del intento anterior.
  • v1 es 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:

  1. Separar la cabecera por comas y leer t y v1.
  2. Calcular el HMAC-SHA256 de <t>.<cuerpo> con el secreto del endpoint.
  3. Comparar el resultado con v1 en tiempo constante.
  4. Rechazar la entrega si t se 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

cabeceracontenido
X-Webhook-Signaturesha256= seguido del HMAC en hexadecimal
X-Webhook-Signature-Timestampedt= y la hora del envío en segundos Unix, una coma y v1= seguido del HMAC en hexadecimal de <t>.<cuerpo>
X-Veriko-EventTipo de evento que originó la entrega, por ejemplo validation.completed
X-Veriko-Delivery-IdIdentificador de la entrega, útil para deduplicar reintentos
X-Veriko-TimestampMomento del envío, en ISO 8601 con sufijo Z
Content-Typeapplication/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"
      }
    }
  }
}
  • code es devuelto o en_proceso_devolucion: los dos estados de Banxico que convierten la validación en returned.
  • reversed es true en los dos casos, y settled es false.
  • checked_at es el instante de la consulta, en ISO 8601 UTC. received_at y processed_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 a validation.completed.
  • No se emite al crear la validación. Una operación que ya llega devuelta se entrega con validation.completed y status: returned; validation.returned es 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:

intentoespera desde el anterior
1.º reintento1 minuto
2.º reintento5 minutos
3.º reintento30 minutos
4.º reintento2 horas
5.º reintento6 horas

No todas las respuestas se reintentan. La distinción es si el fallo puede resolverse con el tiempo:

respuesta del receptortratamiento
2xxEntrega correcta
5xx, error de red o tiempo agotadoSe reintenta
408 y 429Se reintentan: indican saturación temporal
Resto de 4xxNo 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

estadosignificado
pendingEncolada, todavía sin intentar
retryingSe pospuso por el límite de salida; Messenger la reintentará
successEl receptor respondió 2xx
failedEl ú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

  1. Responder 2xx en 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.
  2. Deduplicar por X-Veriko-Delivery-Id: un reintento repite la misma entrega.
  3. Verificar la firma antes de leer el contenido. Con X-Webhook-Signature-Timestamped se puede además rechazar una entrega cuya t se aleje más de 5 minutos del reloj del receptor, sin rechazar los reintentos.
  4. Antes de liberar mercancía o servicio por un validation.completed con veredicto valid, comparar el monto y la cuenta beneficiaria de banxico_confirmed contra los del pedido — ver la sección anterior.
  5. Tratar un valid como reversible durante sus primeras 72 horas y suscribir el endpoint a validation.returned para enterarse de una devolución posterior.