BeeL.es
Product
Pricing
Pricing
← Blog
Development8 min read

Invoicing Webhooks: Real-Time Events with HMAC Verification

Receive invoicing events in real time: invoices issued, paid, VeriFactu status. HMAC-SHA256 verification built into the SDK for enterprise-grade security.

09 April 2026•
Roger Massana
Roger Massana

#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

  1. You configure a URL on your server (endpoint) in the BeeL dashboard
  2. BeeL assigns you a webhook secret (whsec_...)
  3. 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

EventWhen it fires
invoice.createdAn invoice draft is created
invoice.issuedAn invoice is issued (number assigned + VeriFactu)
invoice.paidAn invoice is marked as paid
invoice.sentAn invoice is sent by email
invoice.voidedAn issued invoice is voided
verifactu.status_updatedVeriFactu 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:

  1. Spoofing: someone sends fake events to your endpoint
  2. Replay attacks: someone resends a legitimate old event

#The process

The BeeL-Signature header has the format:

BeeL-Signature: t=1712678400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t = Unix timestamp (seconds) of the send moment
  • v1 = HMAC-SHA256 signature

The SDK verifies:

  1. Parse the header to extract timestamp and signature
  2. Compute HMAC-SHA256(secret, timestamp + "." + body)
  3. Compare the computed signature with v1 using constant-time comparison (prevents timing attacks)
  4. 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

You can view delivery logs in your BeeL dashboard to diagnose issues.

Best practices:

  • Respond with 200 as quickly as possible — process the event asynchronously if it’s heavy
  • Implement idempotency: store the event.id and 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.

Found this useful? Share it with other freelancers

Share:

Ready to simplify your invoicing?

Join BeeL.es and comply with Verifactu hassle-free

7 days free

← View all blog posts
BeeL.es

Our best customer spends 47 seconds per month on BeeL. That's the goal.

Product

  • Blog
  • API
  • API Docs
  • Stripe
  • Accountancy demo
  • Roadmap
  • Status

Comparisons

  • Compare software
  • vs Holded
  • vs Quipu
  • vs Contasimple
  • vs STEL Order
  • vs A3Factura
  • View all →

Verifactu

  • What is Verifactu?
  • Dates and deadlines
  • Freelancer guide
  • Fines and penalties
  • Verifactu articles →
  • By city →

Community

  • LinkedIn
  • Instagram
  • TikTok
  • YouTube

Legal

  • Privacy
  • Cookies
  • Terms
  • Cancellation
  • Responsible Declaration

© 2026 BeeL.es - Made with ❤️ for freelancers like you

BeeL.es - Platja d'Aro, Girona, Spain•hola@beel.es•WhatsApp