New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@beel_es/sdk

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@beel_es/sdk

Official Node.js SDK for BeeL Public API - Spanish invoicing platform with VeriFactu support

latest
Source
npmnpm
Version
2.2.0
Version published
Weekly downloads
600
134.38%
Maintainers
1
Weekly downloads
 
Created
Source

BeeL Node.js SDK

npm version License: MIT TypeScript

Official Node.js/TypeScript SDK for the BeeL API — Spanish invoicing for self-employed professionals with VeriFactu compliance.

Node.js 18+. Single dependency (openapi-fetch).

Reading this from node_modules? Everything you need is in Markdown next to this file: this README for common tasks, docs/reference/README.md for every resource and method (HTTP operation, parameters, return shape, errors), and llms.txt as a short index. You do not need to read dist/index.d.ts.

Features

  • Full TypeScript types generated from the OpenAPI contract, and a typed Markdown reference
  • Automatic retries on 429, 5xx and network errors — only when repeating the request cannot apply it twice
  • Idempotency keys on every POST, reused by that call's retries; createOnce for invoices you must never duplicate
  • Typed errors — catch BeeLNotFoundError instead of checking status codes, read the API's apiCode
  • Webhook signature verification with HMAC-SHA256
  • Instance-based client — multiple API keys, no global state
  • ESM + CommonJS

Installation

npm install @beel_es/sdk

Common tasks

Every example is typed: the request bodies are components['schemas'][...] of the contract, which you can import with import type { components } from '@beel_es/sdk'.

Set up the client

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

// beel_sk_test_* → sandbox (VeriFactu test mode, no quota), beel_sk_live_* → production
const beel = new BeeL({ apiKey: process.env.BEEL_API_KEY! });

Find the company id

Every invoice belongs to a company (a NIF), addressed by its UUID — not by the NIF itself. Your API key tells you its account; the account lists its companies:

const { account_id } = await beel.catalogs.identity();
const { companies } = await beel.account(account_id).companies.list({ search: 'B12345678' });

const company = beel.company(companies[0].id!); // keep this id in your configuration

Create and issue a standard invoice

A create makes a DRAFT; issue numbers it and submits it to VeriFactu. Or do both in one call with options.issue_directly.

import type { components } from '@beel_es/sdk';

type CreateInvoiceRequest = components['schemas']['CreateInvoiceRequest'];

const body: CreateInvoiceRequest = {
  type: 'STANDARD',
  external_ref: 'ORD-2026-0042', // your order id: unique per live invoice, searchable
  recipient: { customer_id: 'customer-uuid' }, // or the recipient's data inline
  lines: [
    {
      description: 'Consulting',
      quantity: 10,
      unit_price: 85.5,
      main_tax: { type: 'IVA', percentage: 21, regime_key: '01' }, // required on every line
    },
  ],
};

const draft = await company.invoices.create(body);
const issued = await company.invoices.issue(draft.id);
console.log(issued.invoice_number); // e.g. "A-2026/0001"

// Or in one call:
const invoice = await company.invoices.create({ ...body, options: { issue_directly: true } });

Create a simplified invoice (ticket)

For sales up to the legal limit without identifying the customer. The recipient may be empty and must not carry a NIF.

const ticket = await company.invoices.create({
  type: 'SIMPLIFIED',
  external_ref: 'TICKET-2026-0815',
  recipient: {},
  lines: [
    { description: 'Menú del día', quantity: 2, unit_price: 14.5, main_tax: { type: 'IVA', percentage: 10, regime_key: '01' } },
  ],
  options: { issue_directly: true },
});

If the customer later asks for a full invoice, exchange it: company.invoices.createSimplifiedExchange({ simplified_invoice_ids: [ticket.id], recipient: { customer_id } }).

Correct an issued invoice

An issued invoice is never edited. If the operation happened but the invoice is wrong, issue a corrective; if it was issued by mistake, void it.

// Partial: only the difference (here, a discount granted after the sale)
const corrective = await company.invoices.createCorrective(issued.id, {
  rectification_type: 'PARTIAL',
  rectification_code: 'R1',
  reason: 'Discount agreed with the customer after the sale',
  lines: [
    { description: 'Discount', quantity: -1, unit_price: 50, main_tax: { type: 'IVA', percentage: 21, regime_key: '01' } },
  ],
});

// Total: cancels everything still invoiced on the original (no lines)
await company.invoices.createCorrective(issued.id, {
  rectification_type: 'TOTAL',
  rectification_code: 'R1',
  reason: 'Order cancelled by agreement with the customer before delivery',
});

// Issued by mistake (a test, an accidental duplicate):
await company.invoices.void(issued.id, { reason: 'Duplicate invoice issued by mistake', issued_in_error: true });

A corrective is not covered by the one-invoice-per-external_ref rule (it carries the order reference of the invoice it corrects), so createOnce does not create correctives. To retry one safely, list by rectified_invoice_id (company.invoices.list({ rectified_invoice_id })) before sending it again, or pass the same idempotencyKey (see below).

Find invoices by your own reference

const { invoices } = await company.invoices.list({ external_ref: 'ORD-2026-0042' });
const invoice = invoices.find((i) => i.type === 'STANDARD' || i.type === 'SIMPLIFIED');

At most one live (not deleted) standard or simplified invoice can hold a given external_ref in each environment: a second create answers 409 with apiCode INVOICE_DUPLICATE_EXTERNAL_REFERENCE.

Read the VeriFactu status

AEAT answers asynchronously: a successful issue means accepted for submission.

const current = await company.invoices.get(invoiceId);
current.verifactu?.submission_status; // 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'VOIDED' | 'NOT_SUBMITTED'
current.verifactu?.qr_url;            // AEAT verification URL, from submission on
current.verifactu?.error_code;        // AEAT's code when it reported a problem

// The records themselves (registration, then cancellation if voided)
const records = await company.invoices.verifactuRecords(invoiceId);

// Everything AEAT rejected
const { invoices: rejected } = await company.invoices.list({ verifactu_status: 'REJECTED' });

To be told instead of polling, subscribe a webhook to verifactu.status.updated (see Webhooks).

Retries, idempotency and never duplicating an invoice

What the client does on its own (defaults: maxRetries: 3, autoIdempotencyKey: true):

SituationAutomatic retry?
429 rate limitYes, after Retry-After, any method. The API refused the request unprocessed.
409 IDEMPOTENCY_KEY_PROCESSINGYes, with the same key: the first request is still running.
5xx or network error on a GETYes, with exponential backoff.
5xx or network error on a POSTYes, with the same Idempotency-Key: if the first attempt took effect, the API replays its answer instead of running it again. A 5xx the API replays (Idempotency-Replay: true) is final and is not retried.
5xx or network error on a PUT/PATCH/DELETE, or a POST sent without a keyNo.
Any other 4xxNo.
  • One call, one key. Every POST gets an Idempotency-Key (a UUID) once, and every automatic retry of that call reuses it. An automatic retry never creates a second invoice.
  • A new call is a new key. If create() finally throws a 5xx or a network error, the outcome is unknown: the invoice may exist. Calling create() again sends a new key and can duplicate it. Do not do that.
  • The API stores the answer of a key for 24 hours (a 2xx or a 5xx; a 4xx is not stored, so the corrected request can reuse the key). Retrying with the same key after a 5xx returns the same 5xx.

The recommended pattern: give the invoice your order id as external_ref, and create it with createOnce. It looks the reference up first, creates only if nothing holds it, and when the create fails with an unknown outcome or a 409 INVOICE_DUPLICATE_EXTERNAL_REFERENCE, it returns the invoice that does exist. Calling it again after any error is safe.

const invoice = await company.invoices.createOnce({
  type: 'STANDARD',
  external_ref: order.id,
  recipient: { customer_id: order.customerId },
  lines: [{ description: 'Order', quantity: 1, unit_price: 100, main_tax: { type: 'IVA', percentage: 21, regime_key: '01' } }],
  options: { issue_directly: true },
});

If you manage keys yourself, persist one per operation and pass it on every attempt:

await company.invoices.create(body, undefined, { idempotencyKey: `order-${order.id}`, timeoutMs: 15_000 });

Errors keep the API's own code in apiCode (code stays the generic class code for backward compatibility):

import { BeeLConflictError, BeeLErrorCodes } from '@beel_es/sdk';

try {
  await company.invoices.create(body);
} catch (error) {
  if (error instanceof BeeLConflictError && error.apiCode === BeeLErrorCodes.INVOICE_DUPLICATE_EXTERNAL_REFERENCE) {
    // someone already created it: look it up by external_ref
  }
  throw error;
}

Companies (multi-NIF)

Every operation on beel.company(companyId) addresses the company in the URL path, so it works with any number of NIFs and never depends on a session focus:

const company = beel.company('company-uuid');

// Invoices — full lifecycle
const { invoices, pagination } = await company.invoices.list({ status: 'ISSUED' });
await company.invoices.issue('invoice-uuid');
// A void is only for an invoice issued by mistake; `issued_in_error: true` is required
// once it has been sent or paid. An operation that did happen gets a corrective instead.
await company.invoices.void('invoice-uuid', { reason: 'Duplicate', issued_in_error: true });
await company.invoices.createCorrective('invoice-uuid', {
  rectification_type: 'PARTIAL',
  rectification_code: 'R1',
  reason: 'Discount granted after the sale',
  circumstance_date: '2026-09-01', // when the art. 80 circumstance happened, if any
  lines: [ ... ],
});
// Exchange issued simplified invoices for a full one with the customer identified
await company.invoices.createSimplifiedExchange({
  simplified_invoice_ids: ['simplified-uuid'],
  recipient: { customer_id: 'customer-uuid' },
});
const records = await company.invoices.verifactuRecords('invoice-uuid'); // registration, cancellation
const pdf = await company.invoices.getPdf('invoice-uuid');

// Customers, products, series
const customer = await company.customers.create({ ... });
const product = await company.products.create({ ... });
await company.series.ensureDefaults(); // idempotent seeding of the typed default series

// Recurring invoices
await company.recurringInvoices.create({ ... });
await company.recurringInvoices.setStatus('rec-uuid', { status: 'PAUSED' });

// Scheduling — one PUT sets or moves it, one DELETE clears it
await company.invoices.schedule.set('invoice-uuid', { scheduled_for: '2026-12-01' });
await company.invoices.schedule.clear('invoice-uuid');

// Derive a new invoice from an existing one
const copy = await company.invoices.derive({ from_invoice_id: 'invoice-uuid' });

// Fiscal configuration of this NIF
const verifactu = await company.verifactuConfiguration.get();
await company.verifactuConfiguration.update({ enabled: true });
const taxes = await company.taxConfiguration.get();

// Company health
const summary = await company.fiscalSummary({ year: 2026 });
const readiness = await company.issuingReadiness();

Whether an invoice reaches AEAT is a fact of the NIF, resolved at issue time — not a per-invoice choice. enabled is the only writable field of the VeriFactu configuration.

Payment connections (Stripe per NIF)

A connection links Stripe to one NIF so its charges auto-generate invoices under it. It is addressed by its own UUID, not by the provider slug — a NIF can hold several connections of the same provider. The slug only appears when you start an authorization.

const { connections } = await company.paymentConnections.list();

const { authorization_url } = await company.paymentConnections.authorize({
  provider: 'stripe',
});

const events = company.paymentConnections.events(connections[0].id);
const { events: pending } = await events.list({ needs_action: true });

// Recovery levers for a charge that did not invoice
await events.retry('event-uuid');
const { invoice_id } = await events.draft('event-uuid'); // review before issuing
await events.resolve('event-uuid');

await company.paymentConnections.disconnect(connections[0].id);

Catalogs and preferences

const taxTypes = await beel.catalogs.taxTypes();
const options = await beel.catalogs.invoiceCustomizationOptions();
await beel.catalogs.updateMe({ language: 'es' }); // the language belongs to the person

Any endpoint: beel.raw

A route reaches the generated types as soon as the contract is synced, but its ergonomic wrapper is written by hand. beel.raw lets you call anything in the contract today, without waiting for a release:

const { data } = await beel.raw.GET('/v1/companies/{company_id}/invoice-customization', {
  params: { path: { company_id: 'company-uuid' } },
});

It is the same client the resources use — authentication, retries and Idempotency-Key still apply — and it stays typed: a path, parameter or body the contract does not have will not compile. What you give up is the sugar and the naming stability of the wrappers.

Error handling is the SDK's too: a non-2xx answer is thrown as a typed error (below), so a raw call never resolves with openapi-fetch's { error } — data is always the success body.

Accounts (managed accounts, members & webhooks)

For integrators managing several accounts (provisioning, gestorías, platforms):

// List and provision managed accounts
const { accounts } = await beel.accounts.list();
const created = await beel.accounts.provision({ external_ref: 'client-42' });

// Scope to one account
const account = beel.account(created.account_id);

// Companies (NIFs) under the account
const { companies } = await account.companies.list();
await account.companies.create({ nif: 'B12345678', ... });

// Members, roles and per-company grants
const members = await account.members.list();
await account.members.putGrant('member-id', 'company-id', { permissions: [...] });

// Invitations
await account.invitations.create({ invited_email: 'user@example.com', account_role: 'MEMBER' });

// Account-scoped webhooks
const webhook = await account.webhooks.create({ url: 'https://...', events: ['invoice.issued'] });
await account.webhooks.test(webhook.id);
await account.webhooks.rotateSecret(webhook.id);

// Usage & ownership
const usage = await account.usage();
await account.createClaimToken(); // let the end user claim the account

Resources (deprecated session-focus surface)

Deprecated: the API contract marks the whole session-focus surface (beel.invoices, beel.customers, beel.products, beel.series, beel.configuration) as deprecated. Prefer the company-scoped equivalents above. These keep working during the sunset window.

// Invoices
const { invoices, pagination } = await beel.invoices.list({ status: 'ISSUED', limit: 20 });
const invoice = await beel.invoices.get('invoice-uuid');
const created = await beel.invoices.create({ ... });
const updated = await beel.invoices.update('id', { ... });
await beel.invoices.delete('id');

// Invoice lifecycle
await beel.invoices.issue('id');
await beel.invoices.markPaid('id');
await beel.invoices.markSent('id');
await beel.invoices.void('id', { reason: 'Duplicate invoice sent in error' });
await beel.invoices.schedule('id', { scheduled_for: '2026-04-15' });
await beel.invoices.unschedule('id');
await beel.invoices.duplicate('id');
await beel.invoices.createCorrective('id', { ... });

// Customers
const { customers, pagination } = await beel.customers.list({ search: 'Acme' });
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' },
});

// Products
const { products } = await beel.products.list();
const results = await beel.products.search('consulting');

// Configuration
const taxConfig = await beel.configuration.getTaxConfig();
const series = await beel.series.list();

// NIF validation
const result = await beel.nif.validate('B12345678');

Typed Errors

Every API error maps to a specific error class:

import { BeeL, BeeLNotFoundError, BeeLValidationError } from '@beel_es/sdk';

try {
  await beel.invoices.get('nonexistent-id');
} catch (error) {
  if (error instanceof BeeLNotFoundError) {
    console.log(error.message);    // "Invoice not found"
    console.log(error.requestId);  // "abc123" (for support)
  }
  if (error instanceof BeeLValidationError) {
    console.log(error.message);    // "El NIF no tiene un formato válido"
    console.log(error.details);    // { field: "nif", invalid_value: "..." }
  }
}
Error classStatusWhen
BeeLAuthError401, 403Invalid or missing API key, or no access to the resource
BeeLNotFoundError404Resource doesn't exist
BeeLValidationError422Invalid data or a business rule
BeeLConflictError409Duplicate (external_ref, NIF…) or an Idempotency-Key conflict
BeeLRateLimitError429Too many requests (retried automatically first)
BeeLApiError400, 5xxBad request, or a server error (retried when safe)

Every error carries statusCode, apiCode (the API's error.code, e.g. INVOICE_DUPLICATE_EXTERNAL_REFERENCE), code (a generic code per class), details and requestId — quote the last one when you contact support. See docs/reference/errors.md.

Automatic Retries

See Retries, idempotency and never duplicating an invoice for exactly what is retried. Tune it with:

const beel = new BeeL({
  apiKey: 'beel_sk_live_...',
  maxRetries: 5,          // default: 3 (0 disables retries)
  retryDelayMs: 1000,     // default: 500
  maxRetryDelayMs: 60000, // default: 30000
});

PDF Download

import fs from 'fs';

const { buffer, fileName } = await beel.downloadPdf('invoice-uuid');
fs.writeFileSync(fileName, buffer);
// => factura_A-2026-0001.pdf

Webhooks

Verify webhook signatures before processing:

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,
    );

    // "Send test" deliveries carry a synthetic `data`: acknowledge and stop.
    if (event.test) return res.status(200).send('OK');

    if (event.type === 'verifactu.status.updated') {
      console.log(event.data.new_status); // 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'VOIDED'
    }

    res.status(200).send('OK');
  } catch {
    res.status(400).send('Invalid signature');
  }
});

Builders

Optional fluent builders for common operations:

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

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

const invoice = InvoiceBuilder.create()
  .forCustomer('customer-uuid')
  .mainTax({ type: 'IVA', percentage: 21, regime_key: '01' }) // required on every line
  .addLine('Service A', 5, 100.0)
  .addLine('Service B', 3, 150.0)
  .metadata({ stripe_id: 'pi_abc123' })
  .build();

Environments

// Sandbox — no VeriFactu quota consumed, safe to test
const sandbox = new BeeL({ apiKey: 'beel_sk_test_...' });

// Production
const prod = new BeeL({ apiKey: 'beel_sk_live_...' });

Full Configuration

const beel = new BeeL({
  apiKey: 'beel_sk_live_...',         // Required
  baseUrl: 'https://app.beel.es/api', // Default
  maxRetries: 3,                       // Default
  retryDelayMs: 500,                   // Default
  maxRetryDelayMs: 30000,              // Default
  autoIdempotencyKey: true,            // Default
});

Documentation

License

MIT

Keywords

beel

FAQs

Package last updated on 28 Sep 2026

Related posts