Agent skill
Belvo Webhooks Skill
Receive and authenticate Belvo open finance webhooks. Use when setting up a Belvo webhook endpoint for Payments (Mexico/Brazil) or Aggregation, checking the optional Authorization header (Bearer or Basic), debugging 401s, or handling events like payment_request_successful, payment_request_failed, consent_confirmed, customer_blocked and historical_update.
Install this skill
npx skills add hookdeck/webhook-skills --skill belvo-webhooks
Belvo (open finance for Latin America) sends webhooks for Payments (Mexico and Brazil) and Aggregation. Payloads are not signed. The only authentication is an optional static credential, sent in the Authorization header, that you set on the webhook.
When to Use This Skill
- How do I receive Belvo webhooks?
- How do I verify the Belvo
Authorizationheader (Bearer or Basic)? - Does Belvo sign webhook payloads? (No: shared credential header only)
- Why does my Belvo webhook endpoint return 401?
- How do I handle
payment_,request_ successful consent_confirmedorhistorical_updateevents?
Verification (core)
In the Belvo portal you can add an authentication value to each webhook. Belvo then sends it as Authorization on every request. On Payments Mexico webhooks you enter a token and Belvo sends Authorization: Bearer <token> (Payments Brazil calls its field "an optional bearer token" but doesn't show the header, so confirm it with a test delivery); on Aggregation webhooks you enter the full value (Bearer <token> or Basic <base64 username:password>) and Belvo sends it unchanged. Store the whole header value, scheme prefix included, and compare it in full. Don't strip or parse the prefix.
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyBelvoRequest(headers, expected = process.env.BELVO_AUTHORIZATION) {
if (!expected) return false; // never accept when unconfigured
const received = headers['authorization']; // full value, e.g. "Bearer 1234-..."
return Boolean(received) && safeEqual(received, expected);
}
Return 401 when this fails, 400 for a body that isn't a JSON object, and 200 once handled. Read the body raw (express.raw({ type: '*/*' })) and parse JSON only after the check passes. Python and Next.js equivalents are in references/verification.md.
A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so a captured request can be replayed and the body can't be proven untampered. Use HTTPS only, rotate the credential if it leaks, and make handlers idempotent.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Shapes
Each product has its own body shape; tell them apart by the fields that are present:
| Product | Shape | Examples |
|---|---|---|
| Payments Mexico | eventType, eventCode, datetime, details | payment_, payment_request_failed, payment_, consent_submitted, consent_confirmed, consent_, consent_rejected, payment_, payment_, payment_, customer_blocked, customer_unblocked, withdrawal_created |
| Payments Brazil, schema V2 | schema_version, resource, resource_id, resource_version, timestamp | resource: BANK_ACCOUNT, CHARGE, CUSTOMER, PAYMENT_AUTHORIZATION |
| Payments Brazil, schema V1 | webhook_id, webhook_type, webhook_code, object_id, external_id, data | CHARGES, ENROLLMENTS and PAYMENT_INTENTS with STATUS_UPDATE; CUSTOMERS and TRANSACTIONS with OBJECT_CREATED |
| Aggregation | webhook_id, webhook_type, webhook_code, link_id, data | historical_update, new_accounts_available, new_, transactions_updated, transactions_deleted, invalid_credentials, token_required, link_deleted |
Brazil sends V2 only for resources created with the X-Belvo-API-Resource-Version: Payments-BR.V2 header, and V1 otherwise. Both Brazil schemas only identify the resource, so fetch it from GET /payments/br/{resource}/{id}/. Brazil V1 and Aggregation share webhook_type/webhook_code: Brazil V1 carries object_id, Aggregation carries link_id. Aggregation webhook_type is the resource (ACCOUNTS, TRANSACTIONS, LINK, CONSENT, ...); see references/overview.md for the full lists.
Retries
Payments Mexico retries only on unreachable, timeout or 5xx responses (every 60 minutes, 3 attempts in total) and never retries 4xx. A 401 from a wrong credential is therefore dropped for good. Payments Brazil retries up to three times, 60 seconds apart, when it gets no 2xx. Aggregation retries five times with linear backoff when it gets no 2xx.
Environment Variables
BELVO_AUTHORIZATION=Bearer 00000000-0000-0000-0000-000000000000 # full header value
Local Development
npx hookdeck-cli listen 3000 belvo --path /webhooks/belvo
No account required. Use the printed URL as the webhook URL in the Belvo portal.
Resources
- references/overview.md - Payments Mexico, Payments Brazil (V1/V2) and Aggregation shapes, event codes, retries
- references/setup.md - Creating a webhook and setting its authentication value
- references/verification.md - Header check in Node, Python and Next.js, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests