Gareth Wilson Gareth Wilson

Guide to Belvo Webhooks: Features and Best Practices

Published


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 eventType and eventCode.
  • Aggregation covers data retrieval for links: accounts, transactions, owners, invoices, consents and more. Bodies carry webhook_type (the resource) and webhook_code (the event).
  • Payments (Brazil) has a V1 format with webhook_type and webhook_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

FeatureDetails
ConfigurationMexico: Direct Debit Portal > Developers > Webhooks. Aggregation: the Data Webhooks tab of the Belvo dashboard. Brazil: the Payments Webhooks tab
Webhooks per accountMexico: up to 5. Brazil: several URLs, each receiving every event
SubscriptionMexico: by eventType, or All events
AuthenticationOptional static value in Authorization. No signature
Mexico headerAuthorization: Bearer <token> for the token you enter
Aggregation headerExactly what you enter: Bearer <token> or Basic <base64 username:password>
Replay protectionNone. No timestamp is signed
Source IPs3.130.254.46, 18.220.61.186, 18.223.45.212
Mexico retriesOnly on unreachable, timeout or 5xx. Every 60 minutes, 3 attempts in all. 4xx is never retried
Aggregation retriesFive retries with linear backoff on any non-2xx
Brazil retriesRespond 2xx within five seconds. Up to three retries, 60 seconds apart
TestingMexico: 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 typeEvent codesSent when
customer_updatecustomer_blocked, customer_unblockedA customer is blocked for suspicious activity, or unblocked after review
consent_updateconsent_submitted, consent_confirmed, consent_incomplete_information, consent_rejectedAll documents are uploaded, the consent is approved, documents are missing or invalid, or the consent is denied
payment_method_updatepayment_method_registration_successful, payment_method_registration_failed, payment_method_registration_canceledA direct debit payment method registration succeeds, fails or is cancelled
payment_request_updatepayment_request_successful, payment_request_failed, payment_request_chargebackA payout is confirmed, the payment infrastructure provider reports an error, or a customer makes a chargeback
withdrawal_updatewithdrawal_createdA 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_transactions_available, 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):

  1. Log in to the Direct Debit Portal for the environment you want (Sandbox or Production).
  2. Go to Developers > Webhooks and click New webhook.
  3. Enter your HTTPS URL.
  4. Select the event types you want, or All events.
  5. Enter your token in the authentication field. Belvo sends it as Authorization: Bearer <token>.
  6. 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 Authorization value 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, not 4xx, for transient problems in your own code.
  • Answer 200 to 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.