Guide to Belvo Webhooks: Features and Best Practices
Belvo webhooks tell your systems when something changes in a customer's open finance data or direct debit payments: a payment request succeeds or fails, a consent is approved or rejected, a payment method is registered, a customer is blocked, or a historical data load for a link finishes. If you build on Belvo in Mexico or Brazil, webhooks are how your application learns about these asynchronous results without polling the API.
This guide covers Belvo's three webhook products and how their payloads differ, how to authenticate requests with the optional Authorization header, the event codes for Mexico direct debit, the retry rules that decide whether a failed delivery is ever sent again, and the best practices for production.
What are Belvo webhooks?
Belvo runs separate webhook systems for separate products, each configured in its own place and each with its own body shape:
- Payments (Mexico) covers direct debit: customers, consents, payment methods, payment requests and withdrawals. Bodies carry
eventTypeandeventCode. - Aggregation covers data retrieval for links: accounts, transactions, owners, invoices, consents and more. Bodies carry
webhook_type(the resource) andwebhook_code(the event). - Payments (Brazil) has a V1 format with
webhook_typeandwebhook_code, and a minimal V2 format that names only the resource and its ID.
None of them is signed. Each webhook can carry an optional static credential in the Authorization header, and comparing that header with the value you set is the only built-in way to tell a Belvo request from a forged one. Belvo also publishes three source IP addresses you can allowlist.
Belvo webhook features
| Feature | Details |
|---|---|
| Configuration | Mexico: Direct Debit Portal > Developers > Webhooks. Aggregation: the Data Webhooks tab of the Belvo dashboard. Brazil: the Payments Webhooks tab |
| Webhooks per account | Mexico: up to 5. Brazil: several URLs, each receiving every event |
| Subscription | Mexico: by eventType, or All events |
| Authentication | Optional static value in Authorization. No signature |
| Mexico header | Authorization: Bearer <token> for the token you enter |
| Aggregation header | Exactly what you enter: Bearer <token> or Basic <base64 username:password> |
| Replay protection | None. No timestamp is signed |
| Source IPs | 3.130.254.46, 18.220.61.186, 18.223.45.212 |
| Mexico retries | Only on unreachable, timeout or 5xx. Every 60 minutes, 3 attempts in all. 4xx is never retried |
| Aggregation retries | Five retries with linear backoff on any non-2xx |
| Brazil retries | Respond 2xx within five seconds. Up to three retries, 60 seconds apart |
| Testing | Mexico: Test on the webhook row, with Resend in the Event delivery panel. Aggregation: trigger any event type from the dashboard |
Common events
Hookdeck's Belvo source type points at the Payments (Mexico) documentation, so this section focuses on that product. You subscribe to an event type, and eventCode names the specific event.
| Event type | Event codes | Sent when |
|---|---|---|
customer_update | customer_blocked, customer_unblocked | A customer is blocked for suspicious activity, or unblocked after review |
consent_update | consent_submitted, consent_confirmed, consent_, consent_rejected | All documents are uploaded, the consent is approved, documents are missing or invalid, or the consent is denied |
payment_method_update | payment_, payment_, payment_ | A direct debit payment method registration succeeds, fails or is cancelled |
payment_request_update | payment_, payment_request_failed, payment_ | A payout is confirmed, the payment infrastructure provider reports an error, or a customer makes a chargeback |
withdrawal_update | withdrawal_created | A withdrawal is created and funds are being settled to you |
Two behaviours are easy to miss. customer_blocked is sent to every merchant, even ones that never registered the customer, because Belvo keeps one block list for all merchants, and each merchant receives it only once per customer. And no consent webhook is sent for the initial awaiting_information status.
A Mexico payload has four top-level fields:
{
"eventType": "payment_request_update",
"eventCode": "payment_request_successful",
"datetime": "2022-01-01T12:34:56.789Z",
"details": {
"id": "3118128a-6792-4b06-bd61-4acf6f6ad6b5",
"reference": "your_reference_here",
"status": "successful",
"amount": 100.5,
"failedReason": null,
"failedMessage": null
}
}
details.id is the Belvo ID of the customer, payment method, payment request or withdrawal. What else sits in details depends on the event: failedReason and failedMessage for failures, documentNumber (an RFC or CURP) for customer events, paymentMethodId and sometimes reasons for consent events, and fromDatetime, toDatetime and extra for withdrawals.
Aggregation events look different. They carry webhook_id, webhook_type (such as ACCOUNTS, TRANSACTIONS or LINK), webhook_code (such as historical_update, new_, invalid_credentials or link_deleted), link_id, external_id and a data object. The same webhook_code recurs across resources, so route on both fields. When retrieval fails, the errors arrive in data.errors.
Setting up Belvo webhooks
Generate a long random token first, for example with openssl rand -hex 32. For Payments (Mexico):
- Log in to the Direct Debit Portal for the environment you want (Sandbox or Production).
- Go to Developers > Webhooks and click New webhook.
- Enter your HTTPS URL.
- Select the event types you want, or All events.
- Enter your token in the authentication field. Belvo sends it as
Authorization: Bearer <token>. - Click Create webhook, then use Test on its row to send a sample payload and check the response in the Event delivery panel.
For Aggregation, open the Data Webhooks tab, click +New webhook, and enter the URL and the authorization value. Here you type the whole header value yourself: Bearer followed by a token, or Basic followed by a base64-encoded username:password. For Brazil, the Payments Webhooks tab takes an optional bearer token.
To route deliveries through Hookdeck, create a Hookdeck source with the Belvo type and use its URL as the webhook URL. The source has one field, Authorization Header, and it must hold the full value Belvo sends, including the scheme prefix. For a Mexico webhook with token abc123, enter Bearer abc123. For an Aggregation webhook, enter exactly what you typed in Belvo's field. The source accepts POST.
For local development, hookdeck listen 3000 belvo --path /webhooks/belvo gives you a public URL that forwards to your machine. Note that some test services, including webhook.site by default, answer with 418, which Belvo treats as a client error and never retries.
Securing Belvo webhooks
Store the whole Authorization value you expect, prefix included, and compare it with the incoming header in constant time. Do not strip or parse the Bearer or Basic prefix. Reject the request if the header is missing or your expected value is not configured.
const crypto = require('crypto');
const express = require('express');
// The full header value, e.g. "Bearer 3f9c...". Never just the token.
const EXPECTED = process.env.BELVO_AUTHORIZATION;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/belvo', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get('authorization');
if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (event.eventCode) {
// Payments (Mexico): route on event.eventCode, key on event.details.id.
} else if (event.webhook_type) {
// Aggregation or Brazil V1: route on webhook_type + webhook_code.
} else if (event.resource) {
// Brazil V2: fetch the resource by event.resource_id for its status.
}
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
EXPECTED = os.environ.get("BELVO_AUTHORIZATION", "") # e.g. "Bearer 3f9c..."
def verify_belvo_request(headers) -> bool:
if not EXPECTED:
return False # never accept when unconfigured
received = headers.get("authorization")
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))
Be clear about the limits of this check. A static header authenticates the sender only. It does not cover the body, so it cannot detect tampering, and with no timestamp a captured request can be replayed and will still pass. Use HTTPS only, a long random token, and rotate it if it might have leaked. The source IP allowlist is a useful second layer at your firewall, but not a substitute for the header.
Make Belvo webhooks production-ready. Hookdeck Event Gateway checks the
Authorizationvalue you configure, queues every payment and aggregation event durably, and lets you retry and replay deliveries long after Belvo has given up.
Belvo webhook limitations and pain points
The token is optional, and it is only a token
The Problem: A Belvo webhook created without a token sends no Authorization header at all, and one created with a token still proves nothing about the body.
Why It Happens: Belvo does not sign its webhooks. The authentication field is optional on all three products, and what it sends is a fixed string.
Workarounds:
- Always set a token, and reject requests without one.
- Allowlist Belvo's three source IPs at your firewall as a second check.
- For money movements, treat the webhook as a notification and confirm the payment request or withdrawal status through the API before acting on it.
How Hookdeck Can Help: Hookdeck's Belvo source compares the full Authorization value you enter. Hookdeck answers Belvo with a 200 either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your destination.
A 4xx in Mexico is final
The Problem: A Payments (Mexico) delivery that gets a 400, 401 or 404 is never sent again, so a bug in your handler or a mismatched token loses the event.
Why It Happens: Belvo retries Mexico webhooks only when your server is unreachable, times out or returns 5xx, and then only every 60 minutes for 3 attempts in all. It treats every 4xx as a client error.
Workarounds:
- Return
5xx, not4xx, for transient problems in your own code. - Answer
200to event codes you do not handle yet, rather than rejecting them. - Reconcile payment request and consent status against the API on a schedule, so a lost webhook is caught.
How Hookdeck Can Help: Hookdeck stores each request as it arrives and delivers it to your service with retries you control, so a bad deploy on your side no longer turns into a lost payment event. You can replay any event once your handler is fixed.
Three products, three shapes
The Problem: A handler written for one product's payloads misreads another's.
Why It Happens: Mexico uses eventType, eventCode and details. Aggregation uses webhook_type, webhook_code and data. Brazil V1 uses webhook_type and webhook_code, while Brazil V2 sends only schema_version, resource, resource_id, resource_version and timestamp, and expects you to fetch the rest.
Workarounds:
- Use a separate endpoint, or a separate Hookdeck source, for each product.
- If you share one endpoint, detect the shape by which fields are present before routing.
How Hookdeck Can Help: Hookdeck filters can route on any body field, so one source can send Mexico events, Aggregation events and Brazil events to different destinations.
Short and inconsistent retry windows
The Problem: An outage of a few hours can outlast every product's retries.
Why It Happens: Mexico tries for about two hours. Brazil retries up to three times 60 seconds apart and expects a response within five seconds. Aggregation retries five times with a linear backoff Belvo does not quantify.
Workarounds:
- Acknowledge quickly and do the work on a queue.
- Alert on endpoint errors so you notice an outage within minutes, not hours.
How Hookdeck Can Help: With Hookdeck in front, Belvo's retry budget is spent against Hookdeck rather than your service. Hookdeck holds each event until your service accepts it, and issues alert you when deliveries fail.
Best practices
Store the whole Authorization value
Keep Bearer <token> or Basic <base64> as one string in your configuration and compare it whole. A stored token of abc123 will never equal an incoming Bearer abc123. Use a separate token per webhook so you can rotate one without touching the others.
Deduplicate on the resource and the event
Retries and multiple webhooks mean the same event can arrive more than once. For Mexico, key on eventCode plus details.id. For Aggregation, key on webhook_id. See our guide to webhook idempotency.
Check status before writing
Payment events are state changes, and retries arrive an hour apart. Before you overwrite a stored status, compare datetime with the last update you recorded, or re-read the resource from the API.
Handle customer_blocked for customers you do not have
Because customer_blocked goes to every merchant, look up details.documentNumber in your own records and ignore the event, with a 200, if the customer is not yours.
Acknowledge fast and process asynchronously
Brazil asks for a response within five seconds, and slow handlers cause timeouts that Belvo counts as failures. Verify the header, store the event, return 200, and process afterwards. See why to process webhooks asynchronously.
Conclusion
Belvo sends unsigned webhooks from three products, each with its own payload shape and retry rules, and authenticates them only with an optional static Authorization value. Always set a token, compare the full header in constant time over HTTPS, allowlist Belvo's IPs, route on eventCode or webhook_type and webhook_code, and never answer a Mexico delivery with a 4xx you would want retried.
Hookdeck Event Gateway checks that header, stores every Belvo event, and retries and replays on your schedule, so a short Belvo retry window stops being the only thing between an outage and a lost payment update.
Get started with Hookdeck for free and handle Belvo webhooks reliably in minutes.