Overview
Receive real-time notifications in your application when events happen in BeeL.
Webhooks let your application receive HTTP notifications the moment something happens in BeeL. — no polling required. When an event occurs (e.g. AEAT processes a VeriFactu submission), BeeL. sends an HTTP POST to the URL you registered with a signed JSON payload.
API plan exclusive: Webhooks are available only with the API plan. See Pricing for details.
Scopes
Every webhook endpoint is reachable with an API key — none of them needs a dashboard
session. Two scopes gate them, webhooks:read for looking and webhooks:write for
changing; each webhook operation states the one it
requires, and you grant them to your key like any other scope.
webhooks:write covers rotating the secret. A key that can create subscriptions
can also invalidate the secret every existing receiver verifies against. If you issue
keys to third parties, that is the reason to keep webhook management on a separate
key from the one that issues invoices.
Quick start
Create a webhook subscription
Requires webhooks:write.
curl -X POST "https://app.beel.es/api/v1/accounts/{account_id}/webhooks" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/beel",
"events": ["invoice.issued", "invoice.email.sent", "invoice.voided", "verifactu.status.updated"]
}'The response contains a secret field — copy it now. It will not be shown again.
{
"success": true,
"data": {
"id": "3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"url": "https://yourapp.com/webhooks/beel",
"events": ["invoice.issued", "invoice.email.sent", "invoice.voided", "verifactu.status.updated"],
"active": true,
"account_relationship": "own",
"secret": "whsec_a3f5b2e1c9d8f7e6b5a4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2",
"consecutive_failures": 0,
"created_at": "2026-03-08T10:00:00Z",
"test_delivery": {
"delivery_success": false,
"http_status": null,
"duration_ms": 3001,
"error": "Connection timed out",
"failure_cause": "timeout"
}
}
}test_delivery is a short signed probe BeeL. sends to your URL while creating the
subscription, so the same call tells you whether your endpoint answers. It never blocks
the registration: the subscription is created and active whatever the probe says, and
test_delivery is null when the probe could not run at all. A failure usually just means
you registered the URL before deploying the endpoint. Branch on failure_cause, not on the
wording of error — the values are the same as in
last_error_cause. Once the endpoint is live,
run a full test.
The URL must be reachable from the internet. A URL whose host resolves to a private,
loopback or otherwise internal address is rejected with
422 URL_TARGET_NOT_ALLOWED, on creation and whenever you
change the URL. A URL that is not https:// is rejected with
URL_MUST_BE_HTTPS. To receive events on your own machine
while you develop, expose the local server through a public HTTPS tunnel and register
the tunnel's URL; a localhost URL and private addresses are rejected like any other.
Managing accounts on someone's behalf? Add "account_relationship": "managed"
(or "all") to also receive events from the accounts you provisioned. It defaults to
own. See Events for how to route them on
arrival.
Store the secret securely! It's only shown once and required to verify webhook signatures.
Handle incoming requests
Your endpoint must:
- Accept
POSTrequests withContent-Type: application/json - Verify the signature using the
BeeL-Signatureheader - Return
HTTP 2xxwithin 10 seconds
// Express.js example
app.post('/webhooks/beel', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['beel-signature'];
if (!verifySignature(req.body, signature, process.env.BEEL_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'invoice.issued':
handleNewInvoice(event.data);
break;
case 'invoice.email.sent':
handleEmailSent(event.data);
break;
case 'invoice.voided':
handleVoided(event.data);
break;
case 'verifactu.status.updated':
handleVeriFactuUpdate(event.data);
break;
}
res.status(200).send('OK');
});Test your endpoint
POST /v1/accounts/{account_id}/webhooks/{webhook_id}/test — webhooks:write. Fires a synthetic,
fully signed payload to your endpoint immediately, apart from the normal deliveries.
curl -X POST "https://app.beel.es/api/v1/accounts/{account_id}/webhooks/3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c/test" \
-H "Authorization: Bearer $BEEL_API_KEY"{
"success": true,
"data": {
"delivery_success": true,
"http_status": 200,
"duration_ms": 142,
"error": null
}
}The call answers 200 whenever the test ran — branch on delivery_success for the
outcome, not on the HTTP status. A delivery your endpoint rejected is a successful test
run, not an error. Test events are not retried and do not appear in the delivery
history.
The same thing is available as the Send test event button in the BeeL. dashboard (Developers → Webhooks → your webhook).
Don't have an endpoint yet? Use webhook.site to get a free temporary URL that captures and displays incoming requests. It's a great way to inspect webhook payloads and verify that events are being delivered correctly before building your handler.
Payload structure
Every webhook event shares the same envelope, with a data block that depends on the event type. The envelope, field by field, is in Events.
Every delivery carries the same envelope fields: id, type, created_at, api_version, livemode, test, company_id, nif, account_id, account_external_ref, account_relationship.
Two of them decide how you write your handler:
idis stable across every retry attempt of the same logical event, so it is the key to deduplicate on — see Deduplication.company_idis what you route on. One endpoint receives the events of every NIF you operate;nullsimply means the event is not about a single company.
Limits
| Parameter | Value |
|---|---|
| Max active subscriptions per account | 10 — paused or turned-off ones do not count. Creating an 11th, or turning a paused one back on when 10 are active, is rejected with 400 WEBHOOK_ACTIVE_SUBSCRIPTION_LIMIT_REACHED |
| Required URL scheme | https:// |
| Delivery log history | Every attempt, paginated — see Delivery logs |
Delivery timeouts and attempt counts are part of the retry policy — see Retries. An endpoint that keeps failing is paused automatically — see Endpoint health.
Headers sent on every delivery
| Header | Description |
|---|---|
Content-Type | application/json |
BeeL-Signature | HMAC-SHA256 signature — see Signatures |
BeeL-Event | Event type (e.g. invoice.issued) |
BeeL-Event-Id | UUID identifying this logical event. Identical across all retry attempts. Matches the id field in the payload. |
BeeL-Delivery-Id | UUID unique to this specific delivery attempt. Matches the delivery log id in the dashboard. |
Idempotency-Key | Same value as BeeL-Event-Id — for automatic deduplication by frameworks |
User-Agent | BeeL-Webhooks |
These are the only headers BeeL. sets. Apart from the ones HTTP itself adds (Host,
Content-Length, Accept), a delivery carries nothing else.
Using the official SDK
The Node.js SDK includes built-in webhook verification:
import { WebhookVerifier } from '@beel_es/sdk';
const verifier = new WebhookVerifier(process.env.BEEL_WEBHOOK_SECRET);
app.post('/webhooks/beel', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = verifier.verify(
req.body.toString('utf8'),
req.headers['beel-signature']
);
// Event is verified and typed
if (event.type === 'verifactu.status.updated') {
console.log('Invoice:', event.data.invoice_number);
console.log('Status:', event.data.new_status);
}
res.status(200).send('OK');
} catch (error) {
res.status(400).send('Invalid signature');
}
});See the SDKs documentation for more details.
Related
Questions? Email us at it@beel.es.