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/sdkpnpm add @beel_es/sdkyarn add @beel_es/sdkbun add @beel_es/sdkRequires 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 prefix | Environment | VeriFactu submission |
|---|---|---|
beel_sk_test_* | Sandbox | Disabled — no real AEAT calls |
beel_sk_live_* | Production | Real 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.
invoices
create, get, list, update, delete, issue, void, createCorrective, markPaid, markSent, revertToIssued, schedule, unschedule, reschedule, getPdf, sendEmail
customers
create, get, list, update, deactivate
products
create, get, list, update, delete, search
series
list, create, update, delete, setDefault
configuration
getTaxConfig, updateTaxConfig, getTaxTypes, getInvoiceCustomizationOptions, updateLanguage, getVerifactu, updateVerifactu
nif
validate(nif) — server-side AEAT lookup
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:
| Field | Description |
|---|---|
pagination.current_page | Page you are on (1-based) |
pagination.total_pages | Total number of pages — iterate up to this, never assume one page |
pagination.total_items | Total matching items across all pages |
pagination.items_per_page | Effective page size |
pagination.has_next / has_previous | Convenience 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.
| Status | Retried? | Notes |
|---|---|---|
| 429 | Yes | Honours the server-provided retry_after (defaults to 60s) |
| 5xx | Yes | Backoff: min(retryDelayMs * 2^attempt + jitter, maxRetryDelayMs) |
| 4xx (other) | No | Client 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.