BeeL
Get StartedVeriFactuStripeAPI Reference

Node.js / TypeScript SDK

Fully-typed TypeScript SDK for the BeeL API with automatic retries, idempotency and built-in webhook verification.


The official BeeL SDK for Node.js 18+ and any modern TypeScript runtime. It wraps the public REST API with strongly-typed resources, fluent builders, automatic retries with exponential backoff, automatic Idempotency-Key generation on every POST, and a constant-time HMAC webhook verifier.

Installation

The SDK has a single runtime dependency (openapi-fetch) and ships its own TypeScript definitions.

npm install @beel_es/sdk
pnpm add @beel_es/sdk
yarn add @beel_es/sdk
bun add @beel_es/sdk

Requires Node.js 18 or higher — the SDK uses the global fetch API and crypto.subtle for webhook verification.

Authentication

Every request must include an API key generated from your BeeL dashboard. The SDK sends it as a Bearer token in the Authorization header.

import { BeeL } from '@beel_es/sdk';

const beel = new BeeL({
  apiKey: process.env.BEEL_API_KEY!,
});
Key prefixEnvironmentVeriFactu submission
beel_sk_test_*SandboxDisabled — no real AEAT calls
beel_sk_live_*ProductionReal submission counts against your quota

The SDK does not read environment variables automatically; always pass apiKey explicitly so credentials remain visible at the call site.

Quickstart

Create a customer, invoice and issue it in three calls.

Initialize the client

import { BeeL } from '@beel_es/sdk';

const beel = new BeeL({ apiKey: process.env.BEEL_API_KEY! });

Create a customer

const customer = await beel.customers.create({
  legal_name: 'Acme SL',
  nif: 'B86561412',
  address: {
    street: 'Calle Mayor',
    number: '1',
    postal_code: '28001',
    city: 'Madrid',
    province: 'Madrid',
    country: 'Spain',
    country_code: 'ES',
  },
});

Create a draft invoice

const draft = await beel.invoices.create({
  type: 'STANDARD',
  recipient: { customer_id: customer.id },
  lines: [
    { description: 'Consulting', quantity: 1, unit_price: 500, discount_percentage: 0 },
  ],
});

Issue the invoice

Assigns the next invoice number, freezes the totals and (for production keys on ES users) submits to VeriFactu.

const issued = await beel.invoices.issue(draft.id);
console.log(issued.invoice_number); // "A-2026/0001"

Resources

The BeeL client exposes one property per resource. Every method returns a typed Promise and throws a typed error on non-2xx responses.

Listing and pagination

List endpoints return the collection wrapped in a pagination envelope. Destructure both:

const { invoices, pagination } = await beel.invoices.list({
  page: 1,
  limit: 50,
  status: 'ISSUED',
});

console.log(`Showing ${invoices.length} of ${pagination.total_items}`);

List endpoints take page (starts at 1) and limit (max 100) query parameters, and return the collection alongside a pagination object:

FieldDescription
pagination.current_pagePage you are on (1-based)
pagination.total_pagesTotal number of pages — iterate up to this, never assume one page
pagination.total_itemsTotal matching items across all pages
pagination.items_per_pageEffective page size
pagination.has_next / has_previousConvenience booleans for cursorless iteration

Downloading PDFs

getPdf returns a pre-signed URL that expires in 5 minutes. downloadPdf is the convenience wrapper that fetches the file and returns a Buffer:

const { buffer, fileName } = await beel.downloadPdf(invoice.id);
await fs.writeFile(fileName, buffer);

Fluent builders

For larger payloads, the SDK ships two chainable builders that validate required fields before they hit the network.

import { InvoiceBuilder } from '@beel_es/sdk';

const payload = InvoiceBuilder.create()
  .type('STANDARD')
  .forCustomer('cust-uuid')
  .addLine('Consulting', 10, 85.50)
  .addLine('Support', 5, 50.00)
  .dueDate('2026-05-31')
  .series('series-uuid')
  .metadata({ stripe_payment_intent: 'pi_abc' })
  .build();

const invoice = await beel.invoices.create(payload);

build() throws if neither forCustomer() nor at least one line have been set.

import { CustomerBuilder } from '@beel_es/sdk';

const payload = CustomerBuilder.create()
  .name('Acme Corporation SL')
  .nif('B12345678')
  .email('contact@acme.com')
  .address('Calle Mayor', '1', '28001', 'Madrid', 'Madrid', 'Spain')
  .build();

const customer = await beel.customers.create(payload);

build() throws if name, nif or address are missing. address(...) defaults countryCode to ES.

Error handling

All errors inherit from BeeLApiError and expose statusCode, code, requestId and optional details. Import the subclasses you care about and narrow with instanceof.

import {
  BeeLApiError,
  BeeLAuthError,
  BeeLNotFoundError,
  BeeLValidationError,
  BeeLConflictError,
  BeeLRateLimitError,
} from '@beel_es/sdk';

try {
  await beel.invoices.get('missing-id');
} catch (err) {
  if (err instanceof BeeLNotFoundError) {
    // 404 — surface a friendly message
  } else if (err instanceof BeeLValidationError) {
    console.log(err.details); // { field: 'message', ... }
  } else if (err instanceof BeeLRateLimitError) {
    console.log(`Retry after ${err.retryAfterSeconds}s`);
  } else if (err instanceof BeeLApiError) {
    console.error(err.requestId, err.code, err.message);
  }
}

Retries and idempotency

The SDK retries transient failures automatically using exponential backoff with jitter, and stamps every POST with a unique Idempotency-Key so it is always safe to retry.

StatusRetried?Notes
429YesHonours the server-provided retry_after (defaults to 60s)
5xxYesBackoff: min(retryDelayMs * 2^attempt + jitter, maxRetryDelayMs)
4xx (other)NoClient errors require fixing the request

Tune the policy at construction time:

const beel = new BeeL({
  apiKey: process.env.BEEL_API_KEY!,
  maxRetries: 5,
  retryDelayMs: 1000,
  maxRetryDelayMs: 60000,
  autoIdempotencyKey: true,
});

A fresh idempotency key is generated for each retry attempt. The server de-duplicates requests within its idempotency window, so retries remain safe end-to-end.

Webhook verification

WebhookVerifier validates the BeeL-Signature header using HMAC-SHA256 with constant-time comparison and rejects replays older than the tolerance window (5 minutes by default).

import express from 'express';
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'] as string,
      );

      if (event.type === 'verifactu.status.updated') {
        const { invoice_number, new_status, qr_url, error } = event.data;
        // ACCEPTED → store qr_url; REJECTED → log error and notify the user
      }

      res.status(200).send('ok');
    } catch {
      res.status(400).send('invalid signature');
    }
  },
);

The body must be the raw bytes as received from BeeL — any reserialization will invalidate the signature. See Webhooks for the full event catalogue and payload shapes.

Configuration reference

Prop

Type

Source and issues

The SDK is open source. Contributions, bug reports and feature requests are welcome.