# 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.

This page describes version **2.2.0** of [`@beel_es/sdk`](https://www.npmjs.com/package/@beel_es/sdk). Everything you do to a company (NIF) goes through `beel.company(companyId)`, which addresses the company in the URL of every request.

## Installation

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

```bash
npm install @beel_es/sdk
```

With another tool: `pnpm add @beel_es/sdk`, `yarn add @beel_es/sdk`, `bun add @beel_es/sdk`.

Use 2.2.0 or later.

<Callout type="info">
  Requires **Node.js 18 or higher**. The SDK uses the global `fetch` API.
</Callout>

## 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.

```ts title="setup.ts"
import { BeeL } from '@beel_es/sdk';

const beel = new BeeL({
  apiKey: process.env.BEEL_API_KEY!,
  // baseUrl: process.env.MY_API_URL, // only for another environment or a proxy
});
```

If your project configures the API URL, pass it as `baseUrl` instead of calling the API by hand; without it the client uses production. Read the key and the URL from your project's own environment variables, whatever their names.

| Key prefix | Environment | VeriFactu submission |
|------------|-------------|----------------------|
| `beel_sk_test_*` | Sandbox | To AEAT's test environment — nothing is registered for real ([details](/verifactu/testing-in-sandbox)) |
| `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.

## Find your company ID

Invoices, customers, products and series belong to a **company** (one NIF). You pass its ID to `beel.company()`. If you don't have it yet, read your account from your identity and list its companies:

```ts title="setup.ts"
const { account_id: accountId } = await beel.catalogs.identity();

const { companies } = await beel.account(accountId).companies.list();
console.log(companies.map((c) => [c.id, c.nif]));
```

The ID doesn't change, so store it in your configuration instead of looking it up on every run. See [Companies](/multi-nif/companies) for how companies are created and switched on.

## Quickstart

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

<Steps>
  <Step>
    ### Scope the client to your company

    ```ts title="quickstart.ts"
import { BeeL } from '@beel_es/sdk';

const beel = new BeeL({ apiKey: process.env.BEEL_API_KEY! });
const company = beel.company(process.env.BEEL_COMPANY_ID!);
```
  </Step>
  <Step>
    ### Create a customer

    ```ts title="quickstart.ts"
const customer = await company.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',
  },
});
```
  </Step>
  <Step>
    ### Create a draft invoice

    ```ts title="quickstart.ts"
const draft = await company.invoices.create({
  type: 'STANDARD',
  recipient: { customer_id: customer.id },
  lines: [
    {
      description: 'Consulting',
      quantity: 1,
      unit_price: 500,
      main_tax: { type: 'IVA', percentage: 21, regime_key: '01' },
      irpf_rate: 0,
    },
  ],
});
```
  </Step>
  <Step>
    ### Issue the invoice

    Assigns the next invoice number, freezes the totals and, when VeriFactu is on for the NIF, submits the record to the AEAT.

    ```ts title="quickstart.ts"
const issued = await company.invoices.issue(draft.id);
console.log(issued.invoice_number); // "A-2026/0001"
```
  </Step>
</Steps>

## Company resources

`beel.company(companyId)` returns a scope with one property per resource. Every method returns a typed `Promise` and throws a [typed error](#error-handling) on non-2xx responses.

<Cards>
  <Card title="invoices" href="/invoices/listCompanyInvoices">
    `list`, `get`, `create`, `createOnce`, `update`, `delete`, `issue`, `void`, `createCorrective`, `createSimplifiedExchange`, `verifactuRecords`, `convertToInvoice`, `setStatus`, `getPdf`, `send`, `derive`, plus `schedule.get` / `schedule.set` / `schedule.clear`
  </Card>
  <Card title="customers" href="/customers/listCompanyCustomers">
    `list`, `get`, `create`, `createBulk`, `update`, `delete`
  </Card>
  <Card title="products" href="/products/listCompanyProducts">
    `list`, `get`, `create`, `update`, `delete`
  </Card>
  <Card title="series" href="/invoice-series/listCompanySeries">
    `list`, `create`, `update`, `delete`, `getDefaults`, `ensureDefaults`, `setDefault`
  </Card>
  <Card title="recurringInvoices" href="/recurring-invoices/listCompanyRecurringInvoices">
    `list`, `get`, `create`, `update`, `delete`, `setStatus`, `skip`, `generateNow`, `nextOccurrence`, `history`
  </Card>
  <Card title="paymentConnections" href="/multi-nif/payment-connections">
    `list`, `authorize`, `update`, `disconnect`, and `events(connectionId)`: `list`, `get`, `retry`, `draft`, `resolve`, `discard`, `restore`
  </Card>
  <Card title="taxConfiguration" href="/tax-configuration/getCompanyTaxConfiguration">
    `get`, `update` (replaces the whole configuration)
  </Card>
  <Card title="verifactuConfiguration" href="/verifactu/getCompanyVeriFactuConfiguration">
    `get`, `update` (`enabled` is the only writable field)
  </Card>
</Cards>

The scope itself reads and manages the company: `get()`, `update()`, `delete()`, `fiscalSummary({ year })` and `issuingReadiness()`, which tells you whether the NIF can issue right now and what is missing if not.

### Listing and pagination

List endpoints return the collection with a `pagination` object. Destructure both:

```ts title="invoices.ts"
const { invoices, pagination } = await company.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 |

### Finding and reading an invoice

`list` takes the same filters as the API, including `external_ref`, the reference you sent when you created the invoice. `get` reads one by id:

```ts title="invoices.ts"
// Your own reference, sent as `external_ref` when you created the invoice
const { invoices: found } = await company.invoices.list({ external_ref: 'ORD-2026-0042' });
const invoice = found[0] ?? (await company.invoices.get(issuedId, { timeoutMs: 10_000 }));
console.log(invoice.status, invoice.verifactu?.submission_status);
```

Every `company.invoices` method takes per-call options as its last argument: `timeoutMs`, `signal` and `idempotencyKey` — see [Retries and idempotency](#retries-and-idempotency).

### Creating an invoice once per order

`createOnce` creates a standard or simplified invoice at most once per `external_ref`: it looks the reference up first, creates only if nothing holds it, and after a `409` [`INVOICE_DUPLICATE_EXTERNAL_REFERENCE`](/errors/INVOICE_DUPLICATE_EXTERNAL_REFERENCE) or an unknown outcome returns the invoice that exists. Calling it again after any error is safe. The invoice returned may be one created earlier, in any status. It does not create correctives. The complete pattern is in [From order to invoice](/guides/order-to-invoice#issue-once).

```ts title="invoices.ts"
// One invoice per order, safe to call again after any error
const orderInvoice = await company.invoices.createOnce(
  {
    type: 'STANDARD',
    external_ref: 'ORD-2026-0042',
    recipient: { customer_id: '4f244735-980b-8d9c-80e8-6331fa0b1958' },
    lines: [
      { description: 'Consulting', quantity: 1, unit_price: 500, main_tax: { type: 'IVA', percentage: 21, regime_key: '01' }, irpf_rate: 0 },
    ],
    options: { issue_directly: true },
  },
  undefined,
  { timeoutMs: 20_000 },
);
```

### Correcting an invoice

`createCorrective` issues a corrective invoice over an issued one. The body is `CreateCorrectiveInvoiceRequest`: which reason code applies and what `TOTAL` and `PARTIAL` do are in [Corrective invoices](/verifactu/corrective-invoices).

```ts title="invoices.ts"
const corrective = await company.invoices.createCorrective(issuedId, {
  rectification_type: 'PARTIAL',
  rectification_code: 'R1',
  reason: 'Discount granted after the sale',
  circumstance_date: '2026-09-01',
  lines: [
    {
      description: 'Discount of 10 % granted after the sale',
      quantity: -1,
      unit_price: 50,
      main_tax: { type: 'IVA', percentage: 21, regime_key: '01' },
    },
  ],
  options: { issue_directly: true },
});
```

`verifactuRecords` lists the records AEAT holds for an invoice — its registration and, if voided, its cancellation:

```ts title="invoices.ts"
const records = await company.invoices.verifactuRecords(issuedId); // registration and cancellation records
```

A customer who wants a full invoice for tickets already issued gets an exchange, not a corrective ([Simplified vs standard](/verifactu/simplified-vs-standard#upgrading-an-f2-to-f1-the-canje-case)):

```ts title="invoices.ts"
// Exchange issued simplified invoices for one standard invoice with the customer identified
const exchange = await company.invoices.createSimplifiedExchange({
  simplified_invoice_ids: [issuedId],
  recipient: { customer_id: '4f244735-980b-8d9c-80e8-6331fa0b1958' },
});
```

### Scheduling an invoice

One call sets the schedule, and calling it again moves it. `generation_mode` is required: `DRAFT` leaves a draft for you to review on that date, `ISSUE_AND_SEND` issues and sends it.

```ts title="invoices.ts"
await company.invoices.schedule.set(draftId, {
  scheduled_for: '2026-12-01',
  generation_mode: 'ISSUE_AND_SEND',
});

const schedule = await company.invoices.schedule.get(draftId);
await company.invoices.schedule.clear(draftId); // back to a plain draft
```

### Deriving an invoice from another

`derive` creates a new draft from an existing invoice:

```ts title="invoices.ts"
const copy = await company.invoices.derive({
  from_invoice_id: issuedId,
  mode: 'DUPLICATE',
});
```

### Downloading PDFs

`getPdf` returns a pre-signed `download_url` that stops working after `expires_in_seconds`. Download it right away:

```ts title="invoices.ts"
import { writeFile } from 'node:fs/promises';

const pdf = await company.invoices.getPdf(issuedId);
const response = await fetch(pdf.download_url);
await writeFile(pdf.file_name, Buffer.from(await response.arrayBuffer()));
```

### Payment connections

A connection links Stripe to one NIF so its charges become invoices under that NIF. Each connection has its **own ID**: a NIF can hold several connections to the same provider. You name the provider only when you start an authorization.

```ts title="payments.ts"
const { authorization_url } = await company.paymentConnections.authorize({
  provider: 'stripe',
});

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

// Charges that did not become an invoice
const { events: pending } = await events.list({ needs_action: true });

await events.retry(pending[0].id); // run it again with the NIF's current configuration
const draft = await events.draft(pending[0].id); // or build a draft to review before issuing
await events.resolve(pending[0].id); // or mark it as handled outside BeeL.
```

`discard` hides an event that needs no action, and `restore` brings it back. See [Payment connections](/multi-nif/payment-connections) for when each lever applies.

## Account resources

These live on the client itself, not on a company:

| Property | Methods |
|----------|---------|
| `beel.account(accountId)` | `get`, `usage`, `changeAccessLevel`, `endManagement`, `createClaimToken`, `setOwner`, and the sub-resources `companies` (`list`, `create`, `stats`), `members` (`list`, `get`, `update`, `remove`, `listGrants`, `putGrant`, `removeGrant`), `invitations` (`list`, `create`, `get`, `revoke`) and `webhooks` (`list`, `create`, `get`, `update`, `delete`, `test`, `rotateSecret`, `listDeliveries`, `retryDelivery`) |
| `beel.accounts` | `list`, `provision`, `get`, `scope` — the accounts you [manage](/multi-nif/managed-accounts) |
| `beel.catalogs` | `taxTypes`, `invoiceCustomizationOptions`, `updateMe` (your personal preferences, such as the language) |
| `beel.nif` | `validate(nif)` — server-side AEAT census lookup |

```ts title="account.ts"
const account = beel.account(accountId);

const webhook = await account.webhooks.create({
  url: 'https://example.com/webhooks/beel',
  events: ['invoice.issued', 'verifactu.status.updated'],
  account_relationship: 'own',
});
```

The response carries the signing secret, which is only returned once. Store it before doing anything else.

## Any endpoint: `beel.raw`

Not every operation has a named method. `beel.raw` is the typed client underneath, so you can call any operation of the contract the installed version was built from:

```ts title="account.ts"
const { data } = await beel.raw.GET('/v1/companies/{company_id}/recurring-invoices/stats', {
  params: { path: { company_id: process.env.BEEL_COMPANY_ID! } },
});
```

Authentication, retries, `Idempotency-Key` and typed errors still apply — `raw` throws the same [typed errors](#error-handling) as the named methods. The path, its parameters and the body are checked against that contract, so a wrong one does not compile. What you give up is the unwrapping: `data` is the whole response body, with the payload under `data.data`.

An operation added to the API after your installed version is not in its types. Update the package to get it, or call it with `fetch` meanwhile.

## Fluent builders

For larger payloads, the SDK ships two chainable builders that validate required fields before they hit the network. Their output goes to the company-scoped `create` methods.

<Tabs items={['InvoiceBuilder', 'CustomerBuilder']}>
  <Tab value="InvoiceBuilder">
    ```ts title="builders.ts"
const payload = InvoiceBuilder.create()
  .type('STANDARD')
  .forCustomer('4f244735-980b-8d9c-80e8-6331fa0b1958')
  .mainTax({ type: 'IVA', percentage: 21, regime_key: '01' }) // for the lines added after it
  .addLine('Consulting', 10, 85.5)
  .addLine('Support', 5, 50)
  .dueDate('2026-05-31')
  .metadata({ stripe_payment_intent: 'pi_abc' })
  .build();

const invoice = await company.invoices.create(payload);
```

    `build()` throws if neither `forCustomer()` nor at least one line have been set.
  </Tab>
  <Tab value="CustomerBuilder">
    ```ts title="builders.ts"
const customerPayload = CustomerBuilder.create()
  .name('Acme Corporation SL')
  .nif('B12345678')
  .email('contact@acme.com')
  .address('Calle Mayor', '1', '28001', 'Madrid', 'Madrid', 'Spain')
  .build();

const customer = await company.customers.create(customerPayload);
```

    `build()` throws if `name`, `nif` or `address` are missing. `address(...)` defaults `countryCode` to `ES`.
  </Tab>
</Tabs>

## Deprecated: the session-focus resources

`beel.invoices`, `beel.customers`, `beel.products`, `beel.series`, `beel.configuration` and `beel.downloadPdf()` call the flat routes (`/v1/invoices`, `/v1/customers`, …), which don't name the company. They are marked `@deprecated` in the SDK and in the contract, and the routes they call **retire on 9 December 2026**. After that date these methods fail.

Move each call to the company scope:

| Deprecated | Use instead |
|------------|-------------|
| `beel.invoices.*` | `beel.company(id).invoices.*` |
| `beel.invoices.schedule()` / `reschedule()` | `company.invoices.schedule.set()`: one call sets or moves the date |
| `beel.invoices.unschedule()` | `company.invoices.schedule.clear()` |
| `beel.invoices.duplicate()` | `company.invoices.derive({ from_invoice_id, mode: 'DUPLICATE' })` |
| `beel.invoices.markPaid()` / `markSent()` / `revertToIssued()` | `company.invoices.setStatus()` with `status: 'PAID'`, `'SENT'` or `'ISSUED'` |
| `beel.invoices.sendEmail()` | `company.invoices.send()` |
| `beel.customers.*` (`deactivate`) | `beel.company(id).customers.*` (`delete`) |
| `beel.products.*` (`search`) | `beel.company(id).products.*` (`list({ q })`) |
| `beel.series.*` | `beel.company(id).series.*` |
| `beel.configuration.getTaxConfig()` / `updateTaxConfig()` | `company.taxConfiguration.get()` / `update()` |
| `beel.configuration.getVerifactu()` / `updateVerifactu()` | `company.verifactuConfiguration.get()` / `update()` |
| `beel.configuration.getTaxTypes()` / `getInvoiceCustomizationOptions()` | `beel.catalogs.taxTypes()` / `invoiceCustomizationOptions()` |
| `beel.configuration.updateLanguage()` | `beel.catalogs.updateMe({ language })` |
| `beel.downloadPdf()` | `company.invoices.getPdf()` and fetch `download_url` ([above](#downloading-pdfs)) |

See [Resources under the NIF](/changelog/resources-under-the-nif) for the full list of retiring routes.

## Error handling

Every non-2xx response is thrown as a `BeeLApiError`, or one of its subclasses, with `statusCode`, `apiCode` (the API's own `error.code`), `message`, `requestId` and, when the API sends them, `details`. Narrow with `instanceof`, and branch on `apiCode`; `BeeLErrorCodes` names the codes the SDK itself relies on. A request that gets no response at all — a network error, or a `timeoutMs` or `signal` that fired — throws the runtime's own error, not a `BeeLApiError`.

```ts title="errors.ts"
import {
  BeeLApiError,
  BeeLConflictError,
  BeeLErrorCodes,
  BeeLNotFoundError,
  BeeLRateLimitError,
  BeeLValidationError,
} from '@beel_es/sdk';

try {
  await company.invoices.get('4f244735-980b-8d9c-80e8-6331fa0b1958');
} catch (err) {
  if (err instanceof BeeLNotFoundError) {
    // 404: surface a friendly message
  } else if (err instanceof BeeLValidationError) {
    console.log(err.apiCode, err.message, err.details); // 422: which rule, and on which field
  } else if (err instanceof BeeLConflictError && err.apiCode === BeeLErrorCodes.INVOICE_DUPLICATE_EXTERNAL_REFERENCE) {
    // 409: an invoice already holds this external_ref — look it up
  } else if (err instanceof BeeLRateLimitError) {
    console.log(`Retry after ${err.retryAfterSeconds}s`); // after the automatic retries ran out
  } else if (err instanceof BeeLApiError) {
    console.error(err.statusCode, err.apiCode, err.requestId, err.message);
  } else {
    throw err; // no response at all: network error, timeout or abort
  }
}
```

| Class | HTTP | `code` (kept for compatibility) |
|-------|------|-----------------|
| `BeeLAuthError` | 401 / 403 | `UNAUTHORIZED` / `FORBIDDEN` |
| `BeeLNotFoundError` | 404 | `NOT_FOUND` |
| `BeeLConflictError` | 409 | `CONFLICT`, with `details` |
| `BeeLValidationError` | 422 | `UNPROCESSABLE_ENTITY`, with `details` |
| `BeeLRateLimitError` | 429 | [`RATE_LIMIT_EXCEEDED`](/errors/RATE_LIMIT_EXCEEDED), with `retryAfterSeconds` |
| `BeeLApiError` | any other status (400, 5xx…) | the API's `error.code` |

`code` is the class's generic value for the statuses with their own class; read `apiCode` for the API's code, such as [`INVOICE_DUPLICATE_EXTERNAL_REFERENCE`](/errors/INVOICE_DUPLICATE_EXTERNAL_REFERENCE) on a `409` or the fiscal rule a `422` enforces.

## Retries and idempotency

The client retries on its own only when repeating the request cannot apply it twice, and every `POST` carries an `Idempotency-Key` (a UUID) that all the automatic retries of that call reuse:

| Situation | Automatic retry? |
|---|---|
| `429` | Yes, after `Retry-After`, any method |
| `409` [`IDEMPOTENCY_KEY_PROCESSING`](/errors/IDEMPOTENCY_KEY_PROCESSING) | Yes, with the same key: the first request is still running |
| `5xx` or network error on a `GET` | Yes, with exponential backoff |
| `5xx` or network error on a `POST` with a key | Yes, **with the same key**: if the first attempt took effect, the API replays its answer. A replayed `5xx` (`Idempotency-Replay: true`) is final |
| `5xx` or network error on `PUT`, `PATCH`, `DELETE`, or a `POST` without a key | No |
| Any other `4xx` | No |

Tune the policy at construction time:

```ts title="retries.ts"
const beel = new BeeL({
  apiKey: process.env.BEEL_API_KEY!,
  maxRetries: 5, // default 3
  retryDelayMs: 1000, // default 500
  maxRetryDelayMs: 60000, // default 30000: the cap of every wait
});
```

A new **call** is a new key. If `create()` finally throws a `5xx` or a network error, the invoice may exist, and calling `create()` again can duplicate it. For invoices, use [`createOnce`](#creating-an-invoice-once-per-order); for anything else, persist one key per operation and pass it on every attempt, with a timeout:

```ts title="retries.ts"
// Per call, on every company.invoices method: your own key, a timeout, an abort signal
await company.invoices.create(body, undefined, {
  idempotencyKey: `order-${order.id}`, // persisted with the order: the same key on every attempt
  timeoutMs: 15_000, // covers the automatic retries too
});
```

Within 24 hours, a repeated key replays the first `2xx` or `5xx` answer instead of running the operation again. See [Idempotency](/guides/idempotency).

## 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). `verify` returns a typed event: after the `test` check, comparing `event.type` narrows `event.data` to that event's payload. Plug the handler into your framework with the raw request body — for Express, `express.raw({ type: 'application/json' })`.

```ts title="webhooks.ts"
import { WebhookVerifier, WebhookVerificationError } from '@beel_es/sdk';

const verifier = new WebhookVerifier(process.env.BEEL_WEBHOOK_SECRET!);

/** Call it with the raw request body (bytes as received) and the BeeL-Signature header. */
export function handleWebhook(rawBody: string | Buffer, signature: string | undefined): number {
  let event;
  try {
    event = verifier.verify(rawBody, signature);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return 400; // bad signature or too old
    throw err;
  }

  // "Send test" deliveries carry synthetic data: acknowledge and stop.
  if (event.test) return 200;

  if (event.type === 'verifactu.status.updated') {
    // `event.data` is narrowed by `type`
    const { invoice_number, operation, new_status, qr_url, error_code, error_message } = event.data;
    // operation: REGISTRATION (the invoice) or VOID (its cancellation)
    // ACCEPTED → store qr_url; REJECTED → log error_code / error_message and notify the user
    console.log(invoice_number, operation, new_status, qr_url, error_code, error_message);
  }
  return 200;
}
```

The body must be the **raw bytes** as received from BeeL — any reserialization will invalidate the signature. See [Webhooks](/webhooks) for the full event catalogue and payload shapes.

## Configuration reference

| Prop | Type | Default | Description |
|---|---|---|---|
| `apiKey` (required) | `string` | — | API credential. Use beel_sk_test_* in sandbox and beel_sk_live_* in production. |
| `baseUrl` | `string` | `https://app.beel.es/api` | API endpoint override. Useful for self-hosted deployments or proxying through a VPN. |
| `maxRetries` | `number` | `3` | Maximum retry attempts for 429 and 5xx responses. |
| `retryDelayMs` | `number` | `500` | Initial backoff delay between retries, in milliseconds. |
| `maxRetryDelayMs` | `number` | `30000` | Maximum backoff delay cap, in milliseconds. |
| `autoIdempotencyKey` | `boolean` | `true` | When true, every POST without an Idempotency-Key gets a UUID, reused by its automatic retries. When false, POSTs carry no automatic key and are not retried on 5xx or network errors. |

## Source and issues

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

<Cards>
  <Card title="View on GitHub" href="https://github.com/beel-es/beel-node-sdk">
    Source code, changelog, examples and issue tracker.
  </Card>
  <Card title="npm package" href="https://www.npmjs.com/package/@beel_es/sdk">
    Latest release, install command and version history.
  </Card>
</Cards>

## Related

<Related>

- [Idempotency](/guides/idempotency) — what the automatic keys protect you from
- [Signature verification](/webhooks/signatures) — the check the SDK does for you
- [Testing VeriFactu in sandbox](/verifactu/testing-in-sandbox) — how the sandbox behaves
- [CLI](/cli) — the same API from the terminal

</Related>

---

Full OpenAPI spec: https://docs.beel.es/api/openapi