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>
  • t is the send time, in Unix seconds (UTC). Each attempt carries its own: a retry does not repeat the previous attempt's timestamp.
  • v1 is 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:

  1. Split the header on commas and read t and v1.
  2. Compute the HMAC-SHA256 of <t>.<body> with the endpoint's secret.
  3. Compare the result with v1 in constant time.
  4. Reject the delivery if t is 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

headercontent
X-Webhook-Signaturesha256= followed by the HMAC in hexadecimal
X-Webhook-Signature-Timestampedt= and the send time in Unix seconds, a comma, and v1= followed by the hexadecimal HMAC of <t>.<body>
X-Veriko-EventEvent type that originated the delivery, for example validation.completed
X-Veriko-Delivery-IdDelivery identifier, useful for deduplicating retries
X-Veriko-TimestampSend time, in ISO 8601 with a Z suffix
Content-Typeapplication/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"
      }
    }
  }
}
  • code is devuelto or en_proceso_devolucion: the two Banxico statuses that turn the validation into returned.
  • reversed is true in both cases, and settled is false.
  • checked_at is the instant of the query, in ISO 8601 UTC. received_at and processed_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.returned in addition to validation.completed.
  • It is not emitted when the validation is created. An operation that already arrives returned is delivered with validation.completed and status: returned; validation.returned is 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:

attemptwait since the previous one
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry6 hours

Not every response is retried. The distinction is whether the failure can resolve itself over time:

receiver responsetreatment
2xxSuccessful delivery
5xx, network error or timeoutRetried
408 and 429Retried: they signal temporary saturation
Any other 4xxNot 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

statemeaning
pendingQueued, not yet attempted
retryingDeferred by the outbound limit; Messenger will retry it
successThe receiver responded 2xx
failedThe 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

  1. Respond 2xx as 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.
  2. Deduplicate by X-Veriko-Delivery-Id: a retry repeats the same delivery.
  3. Verify the signature before reading the content. With X-Webhook-Signature-Timestamped you can also reject a delivery whose t is more than 5 minutes away from the receiver's clock, without rejecting retries.
  4. Before releasing merchandise or service on a validation.completed with a valid verdict, compare banxico_confirmed's amount and beneficiary account against the order — see the section above.
  5. Treat a valid as reversible during its first 72 hours and subscribe the endpoint to validation.returned to learn about a later return.