#Polling doesn’t scale. Webhooks do.
The most common pattern to know if something changed in an external system is polling: asking every X seconds “is there anything new?”. It works, but has real problems:
- Latency: your data is stale until the next poll
- Cost: thousands of empty requests returning “nothing new”
- Rate limits: more requests, closer to the limit
Webhooks invert the model: BeeL notifies you when something happens. Invoice issued, invoice paid, VeriFactu status updated — your server receives an event in real time.
The BeeL API supports webhooks with HMAC-SHA256 verification built into the TypeScript SDK. Enterprise-grade security without implementing cryptography.
#How BeeL webhooks work
- You configure a URL on your server (endpoint) in the BeeL dashboard
- BeeL assigns you a webhook secret (
whsec_...) - When an event occurs, BeeL sends a POST to your URL with:
- Body: JSON with the event data
- Header
BeeL-Signature: HMAC-SHA256 signature for authenticity verification
#Event types
| Event | When it fires |
|---|---|
invoice.created | An invoice draft is created |
invoice.issued | An invoice is issued (number assigned + VeriFactu) |
invoice.paid | An invoice is marked as paid |
invoice.sent | An invoice is sent by email |
invoice.voided | An issued invoice is voided |
verifactu.status_updated | VeriFactu registration status changes |
The verifactu.status_updated event is especially useful: it lets you know when the AEAT has processed and accepted (or rejected) an invoice registration.
#Implementation with Express.js and the SDK
import express from 'express';
import { WebhookVerifier } from '@beel_es/sdk';
const app = express();
const verifier = new WebhookVerifier(process.env.BEEL_WEBHOOK_SECRET!);
// IMPORTANT: express.raw() to receive the body unparsed
app.post('/webhooks/beel', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = verifier.verify(
req.body,
req.headers['beel-signature'] as string
);
switch (event.type) {
case 'invoice.issued':
console.log(`Invoice issued: ${event.data.invoice_number}`);
// Update your system: CRM, accounting, notifications...
break;
case 'invoice.paid':
console.log(`Invoice paid: ${event.data.invoice_number}`);
// Mark as collected in your system
break;
case 'verifactu.status_updated':
console.log(`VeriFactu: ${event.data.status}`);
// Record compliance status
break;
default:
console.log(`Unhandled event: ${event.type}`);
}
res.json({ received: true });
} catch (err) {
console.error('Invalid webhook:', err);
res.status(400).json({ error: 'Invalid signature' });
}
});
app.listen(3000);⚠️Raw body required
The verifier needs the raw body (not parsed by JSON middleware). If you use express.json() globally, exclude the webhook route or use express.raw() specifically.
#How HMAC-SHA256 verification works
Verification protects against two threats:
- Spoofing: someone sends fake events to your endpoint
- Replay attacks: someone resends a legitimate old event
#The process
The BeeL-Signature header has the format:
BeeL-Signature: t=1712678400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt= Unix timestamp (seconds) of the send momentv1= HMAC-SHA256 signature
The SDK verifies:
- Parse the header to extract timestamp and signature
- Compute
HMAC-SHA256(secret, timestamp + "." + body) - Compare the computed signature with
v1using constant-time comparison (prevents timing attacks) - Check freshness: the timestamp must not be older than 5 minutes (configurable)
// Internally, the SDK does this:
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Constant-time comparison — DO NOT use ===
crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);ℹ️Why constant-time comparison?
If you use ===, response time varies based on how many characters match. An attacker can infer the correct signature character by character by measuring response times. timingSafeEqual takes the same time regardless of how many characters match.
#Manual verification (without SDK)
If you don’t use Node.js, you can implement verification in any language:
# Python
import hmac
import hashlib
import time
def verify_webhook(body: bytes, signature_header: str, secret: str):
# 1. Parse header
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = parts['t']
signature = parts['v1']
# 2. Check freshness (5 minutes)
if abs(time.time() - int(timestamp)) > 300:
raise ValueError('Timestamp too old')
# 3. Compute expected signature
payload = f"{timestamp}.{body.decode()}"
expected = hmac.new(
secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()
# 4. Constant-time comparison
if not hmac.compare_digest(expected, signature):
raise ValueError('Invalid signature')#Retries and failure handling
If your server doesn’t respond (timeout or 5xx error), BeeL retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
You can view delivery logs in your BeeL dashboard to diagnose issues.
Best practices:
- Respond with
200as quickly as possible — process the event asynchronously if it’s heavy - Implement idempotency: store the
event.idand discard duplicates - Don’t rely on delivery order — events may arrive out of order
#Example: sync with an accounting system
// Webhook handler that syncs paid invoices with accounting
case 'invoice.paid':
const invoice = event.data;
// Record in accounting system
await accounting.createEntry({
date: invoice.payment_date,
description: `Payment received for invoice ${invoice.invoice_number}`,
debit: { account: '570', amount: invoice.total }, // Cash
credit: { account: '430', amount: invoice.total }, // Accounts receivable
});
// Notify the team
await slack.send('#accounting',
`Invoice ${invoice.invoice_number} paid: ${invoice.total}€`
);
break;#Frequently asked questions
#What events can I receive via webhook?
The main ones: invoice.issued, invoice.paid, invoice.voided, invoice.sent, invoice.created, and verifactu.status_updated. You can filter which events you receive when configuring the webhook.
#What happens if my server doesn’t respond?
BeeL retries with exponential backoff up to 5 times over a ~3 hour period. Delivery logs are available in your dashboard to diagnose issues.
#Why is HMAC signature verification important?
Without verification, anyone who knows your webhook URL could send fake events — for example, marking invoices as paid when they aren’t. HMAC verification ensures only BeeL can send valid events.
#Can I use webhooks without the SDK?
Yes. The SDK simplifies verification with WebhookVerifier, but you can implement HMAC-SHA256 manually in any language. This article includes a Python example.
#Do webhooks work in local development?
Yes, using tools like ngrok or localtunnel to expose your local server to the internet. Configure the temporary ngrok URL as the endpoint in BeeL.
Ready to receive events in real time? Set up your first webhook in the BeeL dashboard, install the SDK (source code on GitHub), and start processing events. The documentation has the complete event type reference, and the SDKs page has quickstarts for TypeScript, Java, and Python. You can also process webhooks with n8n without writing code.
