Webhooks architecture
A webhook is a POST request the API sends to the registered endpoint when a subscribed event occurs. Delivery is asynchronous: the operation that originates the event does not wait for the receiver to respond.
Each endpoint is registered with POST /v1/webhooks, which returns the signing secret. A subscription accepts between 1 and 10 events per endpoint.
Delivery signing
The body of every delivery is signed with HMAC-SHA256 using the endpoint's secret. The signature travels in hexadecimal, prefixed by the algorithm:
X-Webhook-Signature: sha256=<hex>Verification consists of computing the HMAC-SHA256 of the body exactly as received, without re-serialising the JSON, and comparing it against the header value. A constant-time comparison avoids leaking information through response timing.
Signature with a timestamp
Every delivery also carries a second signature that includes the send time in what is signed:
X-Webhook-Signature-Timestamped: t=<seconds>,v1=<hex>tis the send time, in Unix seconds (UTC). Each attempt carries its own: a retry does not repeat the previous attempt's timestamp.v1is the HMAC-SHA256, in hexadecimal, of the string<t>.<body>with the endpoint's secret. The body is the one received exactly as it arrived.
The X-Webhook-Signature header keeps being sent unchanged: anyone already verifying with it has nothing to modify. The X-Veriko-Timestamp value is not signed; t is, and it is what lets a receiver discard an old delivery that a third party sends again.
To verify it:
- Split the header on commas and read
tandv1. - Compute the HMAC-SHA256 of
<t>.<body>with the endpoint's secret. - Compare the result with
v1in constant time. - Reject the delivery if
tis more than 5 minutes away from the receiver's clock.
The window does not replace deduplication: the Delivery-Id stays stable across retries, and it is worth keeping the received identifiers for at least 24 hours.
Delivery headers
| header | content |
|---|---|
X-Webhook-Signature | sha256= followed by the HMAC in hexadecimal |
X-Webhook-Signature-Timestamped | t= and the send time in Unix seconds, a comma, and v1= followed by the hexadecimal HMAC of <t>.<body> |
X-Veriko-Event | Event type that originated the delivery, for example validation.completed |
X-Veriko-Delivery-Id | Delivery identifier, useful for deduplicating retries |
X-Veriko-Timestamp | Send time, in ISO 8601 with a Z suffix |
Content-Type | application/json; charset=utf-8 |
The signature header is generic and configured independently of the service name. The timestamped signature one adds the -Timestamped suffix to that same name. The other three are formed from the service identifier. The different prefixes are deliberate.
The Delivery-Id stays stable across retries of the same delivery: it is the value that lets the receiver discard duplicates.
Body of a validation delivery
The body is JSON and follows the same envelope across the seven validation events: validation.completed, validation.failed, validation.error, validation.returned and the three 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 is the reference the integrator sent when creating the validation, returned as sent. It only appears when the request included it.
attributes.candidate_match (index from 0 and account_last4) says which of the cuentas_candidatas sent won. It only appears when the request carried that list and an account matched; the full account does not travel in the event.
attributes.banxico_confirmed only appears once Banxico confirmed the CEP — with banxico_status: valid, or returned when the receipt was downloaded before the reversal — and carries what Banxico determined was actually paid: the amount, the operation date and time, the tracking key, the sending and receiving bank, and the beneficiary account. The account travels masked to the last 4 digits; the block never includes names or tax IDs.
A later return: validation.returned
A valid is the verdict of the moment of the query: the CEP proves that the transfer settled, not that the money stayed in the beneficiary's account. If Banxico reports the return afterwards, the validation moves from valid to returned and validation.returned is emitted. That happens in a follow-up check, which is requested with POST /v1/validations/{id}/recheck or which the service runs by itself during the first 72 hours.
The event carries the same body as validation.completed — client_ref and banxico_confirmed included — and adds attributes.payment_status, the payment status that gave the return away:
{
"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"
}
}
}
}codeisdevueltooren_proceso_devolucion: the two Banxico statuses that turn the validation intoreturned.reversedistruein both cases, andsettledisfalse.checked_atis the instant of the query, in ISO 8601 UTC.received_atandprocessed_at, when Banxico supplies them, are its local time, as is.
Three details that matter when integrating:
- The endpoint receives only the events it subscribed to: to learn about a later return, subscribe it to
validation.returnedin addition tovalidation.completed. - It is not emitted when the validation is created. An operation that already arrives returned is delivered with
validation.completedandstatus: returned;validation.returnedis the later transition. - It goes out once per validation, even if the check is repeated. The CEP is kept and stays available through the event's links.
A validation that is still valid after 72 hours is not guaranteed: the service stops checking it, but a later return is still possible. For payment control, bank reconciliation remains the second line.
Timeouts and retries
The connection is allowed 5 seconds and the complete request 10 seconds. Past that margin the delivery counts as failed.
A failed delivery is retried up to 5 times, with growing waits:
| attempt | wait since the previous one |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 6 hours |
Not every response is retried. The distinction is whether the failure can resolve itself over time:
| receiver response | treatment |
|---|---|
2xx | Successful delivery |
5xx, network error or timeout | Retried |
408 and 429 | Retried: they signal temporary saturation |
Any other 4xx | Not retried. A 400 or a 404 signals a misconfigured receiver, and repeating the request does not fix it |
The receiver's response body is kept truncated to 2,048 bytes for the delivery history, available at GET /v1/webhooks/{id}/deliveries.
Outgoing deliveries have two per-minute caps towards a single destination host: the one of each account, set by its plan (from 10 on free to 300 on volumen), and a global cap of 600 that all accounts add up to. Each account counts on its own, so several customers pointing at the same host do not draw down each other. A receiver with many simultaneous events gets them staggered rather than in a burst: a delivery that does not fit stays in retrying and is retried on the schedule above, without being lost. The values that apply to the account are returned by GET /v1/usage/limits, and the full ladder is in rate limits.
Automatic deactivation
The endpoint keeps a consecutive-failure counter. A successful delivery resets it to zero.
On reaching 3 consecutive failures, the endpoint moves to auto_disabled state and stops receiving deliveries. The state is available at GET /v1/webhooks alongside the consecutive_failures counter.
Reactivation is manual, via PUT /v1/webhooks/{id} setting the state to active. The auto_disabled value cannot be assigned through the API — only the system sets it.
Delivery states
| state | meaning |
|---|---|
pending | Queued, not yet attempted |
retrying | Deferred by the outbound limit; Messenger will retry it |
success | The receiver responded 2xx |
failed | The latest attempt failed; Messenger can schedule another one for a recoverable failure |
A delivery that was about to be sent (pending or retrying) when its validation was purged with POST /v1/validations/{id}/purge/execute moves to failed with error_message set to validation_purged and is not sent. Of the deliveries of a purged validation, the record is kept, with its event, status, attempts, and dates, without the sent body or the receiver response, and a purged validation emits no new events.
Receiver recommendations
- Respond
2xxas soon as the body is received and validated, and process it afterwards. A receiver that processes before responding eats into the 10-second margin and triggers unnecessary retries. - Deduplicate by
X-Veriko-Delivery-Id: a retry repeats the same delivery. - Verify the signature before reading the content. With
X-Webhook-Signature-Timestampedyou can also reject a delivery whosetis more than 5 minutes away from the receiver's clock, without rejecting retries. - Before releasing merchandise or service on a
validation.completedwith avalidverdict, comparebanxico_confirmed's amount and beneficiary account against the order — see the section above. - Treat a
validas reversible during its first 72 hours and subscribe the endpoint tovalidation.returnedto learn about a later return.