NewStripe: a buyer with a NIF always gets a standard invoice, and the simplified threshold is deprecated
BeeL
Get startedMulti-NIFVeriFactuRulesStripeAPI referenceChangelog
Webhooks

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 POST requests with Content-Type: application/json
  • Verify the signature using the BeeL-Signature header
  • Return HTTP 2xx within 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:

  • id is stable across every retry attempt of the same logical event, so it is the key to deduplicate on — see Deduplication.
  • company_id is what you route on. One endpoint receives the events of every NIF you operate; null simply means the event is not about a single company.

Limits

ParameterValue
Max active subscriptions per account10 — 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 schemehttps://
Delivery log historyEvery 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

HeaderDescription
Content-Typeapplication/json
BeeL-SignatureHMAC-SHA256 signature — see Signatures
BeeL-EventEvent type (e.g. invoice.issued)
BeeL-Event-IdUUID identifying this logical event. Identical across all retry attempts. Matches the id field in the payload.
BeeL-Delivery-IdUUID unique to this specific delivery attempt. Matches the delivery log id in the dashboard.
Idempotency-KeySame value as BeeL-Event-Id — for automatic deduplication by frameworks
User-AgentBeeL-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.


Questions? Email us at it@beel.es.