@beel_es/sdk 1.3.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +227 -0
- package/README.md +237 -29
- package/dist/index.d.mts +4875 -1235
- package/dist/index.d.ts +4875 -1235
- package/dist/index.js +1462 -184
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1460 -184
- package/dist/index.mjs.map +1 -1
- package/docs/reference/README.md +233 -0
- package/docs/reference/account-companies.md +55 -0
- package/docs/reference/account-invitations.md +57 -0
- package/docs/reference/account-members.md +97 -0
- package/docs/reference/account-webhooks.md +123 -0
- package/docs/reference/account.md +94 -0
- package/docs/reference/accounts.md +72 -0
- package/docs/reference/catalogs.md +68 -0
- package/docs/reference/client.md +113 -0
- package/docs/reference/company-customers.md +85 -0
- package/docs/reference/company-invoices-schedule.md +52 -0
- package/docs/reference/company-invoices.md +316 -0
- package/docs/reference/company-payment-connections-events.md +103 -0
- package/docs/reference/company-payment-connections.md +87 -0
- package/docs/reference/company-products.md +72 -0
- package/docs/reference/company-recurring-invoices.md +136 -0
- package/docs/reference/company-series.md +94 -0
- package/docs/reference/company-tax-configuration.md +36 -0
- package/docs/reference/company-verifactu-configuration.md +41 -0
- package/docs/reference/company.md +94 -0
- package/docs/reference/configuration.md +90 -0
- package/docs/reference/customer-builder.md +107 -0
- package/docs/reference/customers.md +91 -0
- package/docs/reference/errors.md +72 -0
- package/docs/reference/invoice-builder.md +149 -0
- package/docs/reference/invoices.md +250 -0
- package/docs/reference/nif.md +31 -0
- package/docs/reference/products.md +111 -0
- package/docs/reference/series.md +81 -0
- package/docs/reference/types.md +222 -0
- package/docs/reference/webhook-verifier.md +95 -0
- package/llms.txt +52 -0
- package/package.json +11 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the BeeL Node.js SDK will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [2.1.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Automatic retries now happen.** `maxRetries`, `retryDelayMs` and `maxRetryDelayMs` had no
|
|
14
|
+
effect: the retry hook only ran when `fetch` itself rejected, and then ignored the error, so
|
|
15
|
+
no request was ever retried. Retries now run in a wrapper around `fetch`, and only when
|
|
16
|
+
repeating the request cannot apply it twice: `429` (after `Retry-After`) for any method;
|
|
17
|
+
`409 IDEMPOTENCY_KEY_PROCESSING` with the same key; `5xx` and network errors for `GET` and
|
|
18
|
+
for requests that carry an `Idempotency-Key`. A `5xx` the API replays for a key
|
|
19
|
+
(`Idempotency-Replay: true`) is not retried. Every wait is capped at `maxRetryDelayMs`.
|
|
20
|
+
- **A retry reuses the call's `Idempotency-Key`.** The previous retry code, had it run, set a
|
|
21
|
+
new key on each attempt, which could create a second invoice. One call now sends one key on
|
|
22
|
+
every attempt.
|
|
23
|
+
- **`autoIdempotencyKey: false` is honoured.** It was declared but never read.
|
|
24
|
+
- **Typed errors keep the API's error code.** `BeeLConflictError`, `BeeLValidationError` and the
|
|
25
|
+
other subclasses replaced `error.code` with a generic value, so codes such as
|
|
26
|
+
`INVOICE_DUPLICATE_EXTERNAL_REFERENCE` were lost. They are now in the new `apiCode` field
|
|
27
|
+
(`code` is unchanged). `BeeLConflictError` also carries `details`.
|
|
28
|
+
- **Request fields the API defaults are optional in the generated types.** Fields such as a
|
|
29
|
+
line's `line_type` and `discount_percentage`, or `options.issue_directly`, were typed as
|
|
30
|
+
required because they have a default. On schemas that only travel in requests they are now
|
|
31
|
+
optional, as the contract says; response types are unchanged.
|
|
32
|
+
- **`InvoiceBuilder` can set the tax of its lines.** `addLine` never set `main_tax`, which the
|
|
33
|
+
API requires on every normal line. New `mainTax(tax)` sets it for the lines added after it,
|
|
34
|
+
and `addLine` takes an optional per-line `mainTax`.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `company.invoices.createOnce(body)`: creates a standard or simplified invoice once per
|
|
38
|
+
`external_ref` — looks it up first, and after a `409 INVOICE_DUPLICATE_EXTERNAL_REFERENCE`
|
|
39
|
+
or an unknown outcome returns the invoice that exists. Safe to call again after any error.
|
|
40
|
+
- Per-call `RequestOptions` on every `company.invoices` method: `idempotencyKey`, `signal`
|
|
41
|
+
and `timeoutMs` (the timeout covers the automatic retries).
|
|
42
|
+
- `beel.catalogs.identity()` (`GET /v1/me/identity`): the credential's `account_id`, the
|
|
43
|
+
starting point to list its companies.
|
|
44
|
+
- `BeeLErrorCodes`, `EXTERNAL_REF_UNIQUE_INVOICE_TYPES` and the types `RequestOptions`,
|
|
45
|
+
`CreateInvoiceOnceRequest`, `CreateInvoiceOnceParams`, `ExternalRefUniqueInvoiceType`,
|
|
46
|
+
`BeeLErrorCode`.
|
|
47
|
+
- A typed Markdown reference in `docs/reference/` (one page per resource, one anchor per
|
|
48
|
+
method, an index of every method) and `llms.txt`, generated from the source and the
|
|
49
|
+
contract with `npm run docs:reference` and shipped in the package. `npm test` fails when
|
|
50
|
+
it is stale or a public method lacks JSDoc.
|
|
51
|
+
- README: a *Common tasks* section, and the exact retry and idempotency semantics.
|
|
52
|
+
|
|
53
|
+
## [2.0.0] - 2026-09-27
|
|
54
|
+
|
|
55
|
+
Contract resynced with the API as it runs in production today (public contract 1.9.0) and
|
|
56
|
+
types regenerated. The major bump is because several **generated types narrow or lose
|
|
57
|
+
fields**, not because any method changed name: most integrations compile unchanged.
|
|
58
|
+
|
|
59
|
+
### Changed (BREAKING)
|
|
60
|
+
- **Series need a `document_type`.** Creating a series (`company.series.create`, and the
|
|
61
|
+
series in an account import) now requires `document_type`: each series numbers one kind
|
|
62
|
+
of document.
|
|
63
|
+
- **Equivalence surcharge and IRPF rates follow the current tables.** `EquivalenceSurchargePercentage`
|
|
64
|
+
is `0 | 0.26 | 0.5 | 0.62 | 1 | 1.4 | 1.75 | 5.2` (the old `0.625` is `0.62`).
|
|
65
|
+
`IrpfPercentage` adds `2.8`, `6`, `7.6` and `9.5` and is now a `number`, so a line's
|
|
66
|
+
`irpf_rate` is no longer typed as an integer.
|
|
67
|
+
- **`verifactu.chaining_hash` is gone from the invoice.** The API no longer returns it.
|
|
68
|
+
- **`accounts.provision()` returns what the API returns**: `ProvisionAccountResult`
|
|
69
|
+
(`account_id`, `status`, `claim_token`, `claim_url`, `claim_link_already_issued`), not a
|
|
70
|
+
`ManagedAccountSummary`. The old type described fields the response never carried. Call
|
|
71
|
+
`beel.accounts.get(accountId)` for the summary.
|
|
72
|
+
- **Webhook event types are usable.** Every generated event type (`InvoiceIssuedEvent`, …)
|
|
73
|
+
collapsed to `never`, so `if (event.type === '...')` narrowed to nothing. They are now
|
|
74
|
+
plain types with `type` as the literal the API sends, and a real event has `test` absent
|
|
75
|
+
or `false`: check `if (event.test) return;` before narrowing on `type`.
|
|
76
|
+
`WebhookEventEnvelope` types `type` as a string literal union instead of the enum.
|
|
77
|
+
- Schemas the contract dropped are no longer in `components`: `IgicPercentage`,
|
|
78
|
+
`IpsiPercentage`, `IrpfCategory`, `OtherTaxPercentage`, `VatCategory`.
|
|
79
|
+
- `phone` on company and customer updates is typed by `PhoneInput` (the input rules); the
|
|
80
|
+
`Phone` returned in responses no longer carries a minimum length or pattern.
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- `company.invoices.createSimplifiedExchange()` — issues a full invoice in exchange for one or
|
|
84
|
+
more issued simplified invoices (`POST .../invoices/simplified-exchanges`). The simplified
|
|
85
|
+
invoices become `VOIDED` with `void_cause: 'EXCHANGED'`; the new invoice lists them in
|
|
86
|
+
`replaced_invoice_ids`.
|
|
87
|
+
- `company.invoices.verifactuRecords(invoiceId)` — the VeriFactu records of an invoice
|
|
88
|
+
(registration and, if voided, cancellation), each with its own status.
|
|
89
|
+
- Voids take `issued_in_error` (required as `true` once the invoice was sent or paid).
|
|
90
|
+
- Correctives take `circumstance_date`, `recipient` and `recipient_is_business`.
|
|
91
|
+
- Exemption reasons `ISP_ART_84_2_B`, `_C`, `_D` and `_G`; tax rates and surcharges carry
|
|
92
|
+
`valid_from` / `valid_until`; tax configuration returns `withholding_options`.
|
|
93
|
+
- Recurring invoices return `is_failing` and `last_generated_scheduled_date`; their history
|
|
94
|
+
entries carry `type`, `origin`, `reason` and who requested them.
|
|
95
|
+
- Webhooks `invoice.pdf.generated` and `invoice.schedule_failed`: `InvoicePdfGeneratedEvent`,
|
|
96
|
+
`InvoiceScheduleFailedEvent` and their `WebhookEventData*` types, included in `WebhookEvent`.
|
|
97
|
+
`VeriFactuSubmissionStatus` now includes `PENDING`, sent when the record is submitted.
|
|
98
|
+
- Error codes documented by the contract for the flows above (for example
|
|
99
|
+
`VOID_REQUIRES_ISSUED_IN_ERROR`, `CORRECTIVE_OUT_OF_TIME`, `SIMPLIFIED_NOT_EXCHANGEABLE`,
|
|
100
|
+
`EXCHANGE_INVOICE_NOT_VOIDABLE`, `SERIES_DOCUMENT_TYPE_LOCKED_HAS_INVOICES`). They reach
|
|
101
|
+
you as `error.code` on the typed errors, as before.
|
|
102
|
+
- `npm run typecheck`.
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
- The `beel.raw` example called `GET /v1/companies/{company_id}/logo`, which does not exist
|
|
106
|
+
(the logo is only `PUT` and `DELETE`); it now uses `GET .../invoice-customization`.
|
|
107
|
+
|
|
108
|
+
## [1.3.0] - 2026-09-17
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
- **Payment connections per NIF**: `beel.company(id).paymentConnections` — list, start an
|
|
112
|
+
authorization, update and disconnect. A connection is addressed by **its own UUID**, not by
|
|
113
|
+
the provider slug: a NIF can hold several connections of the same provider. The slug is only
|
|
114
|
+
sent when starting an authorization.
|
|
115
|
+
- **Payment events of a connection**: `...paymentConnections.events(connectionId)` — list with
|
|
116
|
+
the full filter set, read one, and the five recovery levers for a charge that did not invoice:
|
|
117
|
+
`retry`, `draft` (builds a draft invoice to review before issuing), `resolve`, `discard`
|
|
118
|
+
and `restore`.
|
|
119
|
+
- **Fiscal configuration per company**: `beel.company(id).verifactuConfiguration` and
|
|
120
|
+
`.taxConfiguration`. These are the canonical replacements the `beel.configuration` JSDoc
|
|
121
|
+
already pointed at, which did not exist until now. In VeriFactu, `enabled` is the only
|
|
122
|
+
writable field: whether an invoice reaches AEAT is a fact of the NIF resolved at issue time.
|
|
123
|
+
- **Invoice schedule**: `beel.company(id).invoices.schedule` — one `set()` and one `clear()`
|
|
124
|
+
replace the legacy `schedule`, `reschedule` and `unschedule` calls.
|
|
125
|
+
- **Invoice derivation**: `beel.company(id).invoices.derive()`, the canonical form of `duplicate`.
|
|
126
|
+
- **Catalogs and preferences**: `beel.catalogs.taxTypes()`, `.invoiceCustomizationOptions()`
|
|
127
|
+
and `.updateMe()`, on their canonical paths instead of the deprecated `/v1/configuration/*`.
|
|
128
|
+
- **`beel.raw`**: the typed client underneath, for any endpoint the SDK does not wrap yet. A new
|
|
129
|
+
route reaches the generated types as soon as the contract is synced, but its ergonomic wrapper
|
|
130
|
+
is written by hand — so without this you had to wait for a release to call it. Same
|
|
131
|
+
authentication, retries and `Idempotency-Key`, and still typed: a path, parameter or body the
|
|
132
|
+
contract does not have will not compile.
|
|
133
|
+
- `check:coverage` guard: a ratchet that fails the build when the contract grows a live
|
|
134
|
+
operation the SDK does not wrap. The current set is recorded in
|
|
135
|
+
`scripts/coverage-baseline.json`, so it does not block today, but the contract can no longer
|
|
136
|
+
grow unnoticed.
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
- `beel.accounts.scope(id).companies.list()` read the collection as a bare array with the
|
|
140
|
+
pagination beside it. The contract wraps it (`data.companies` + `data.pagination`), so the
|
|
141
|
+
call did not type-check against the current API. Its public return shape is unchanged.
|
|
142
|
+
- CI: the auto-regenerate workflow depended on a `branch` field the backend never sends, so
|
|
143
|
+
only the spec sync ever ran and the generated types were never refreshed. It now regenerates
|
|
144
|
+
and runs the contract guards on every sync that brings changes.
|
|
145
|
+
- `npm publish` ran neither the tests nor the contract guards; `prepublishOnly` now does.
|
|
146
|
+
|
|
147
|
+
### Changed
|
|
148
|
+
- OpenAPI contract synced with the published API (the previous copy was five weeks behind);
|
|
149
|
+
types regenerated.
|
|
150
|
+
|
|
151
|
+
## [1.2.0] - 2026-08-11
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
- **Accounts (multi-account provisioning)**: `beel.accounts` — list/get/provision managed
|
|
155
|
+
accounts, plus `beel.accounts.scope(accountId)` for members (roles and per-company grants),
|
|
156
|
+
invitations, account-scoped webhook subscriptions (secret rotation, deliveries, retries, test),
|
|
157
|
+
companies under an account, usage counters, claim tokens, ownership transfer and access level.
|
|
158
|
+
- **Company scope (multi-NIF)**: `beel.company(companyId)` — invoices (full lifecycle: create,
|
|
159
|
+
issue, void, corrective, convert proforma, status, PDF, send), customers (incl. bulk), products,
|
|
160
|
+
series (incl. typed defaults seeding), recurring invoices, fiscal summary and issuing readiness.
|
|
161
|
+
All operations address the company in the path; no session focus needed.
|
|
162
|
+
- Webhook event catalog expanded from the contract: `invoice.issued`, `invoice.voided`,
|
|
163
|
+
`invoice.email.sent`, `recurring_invoice.paused`, `account.claimed`, `company.created`,
|
|
164
|
+
`representation.signed` and test events, all exported and included in `WebhookEvent`.
|
|
165
|
+
- `check:deprecated` drift guard: fails the build if the SDK wraps an operation the OpenAPI
|
|
166
|
+
contract marks `deprecated: true` without a matching `@deprecated` tag (or vice versa).
|
|
167
|
+
|
|
168
|
+
### Changed
|
|
169
|
+
- OpenAPI contract synced from backend `develop` (v1.7.1); types regenerated.
|
|
170
|
+
- Invoice lines now require `line_type` (`NORMAL` | `SUPLIDO`); `InvoiceBuilder.addLine()`
|
|
171
|
+
defaults to `NORMAL`.
|
|
172
|
+
|
|
173
|
+
### Deprecated
|
|
174
|
+
- `VeriFactuStatusUpdatedData` and `VeriFactuStatus` type names, renamed in the contract to
|
|
175
|
+
`WebhookEventDataVeriFactuStatusUpdated` and `VeriFactuSubmissionStatus`. Deprecated aliases
|
|
176
|
+
are kept so existing imports keep compiling.
|
|
177
|
+
- The whole session-focus surface (`beel.invoices`, `beel.customers`, `beel.products`,
|
|
178
|
+
`beel.series`, `beel.configuration`, `beel.downloadPdf`), matching the contract. Use the
|
|
179
|
+
company-scoped equivalents on `beel.company(companyId)`.
|
|
180
|
+
|
|
181
|
+
## [Unreleased]
|
|
182
|
+
|
|
183
|
+
## [2.1.0] - 2026-09-28
|
|
184
|
+
|
|
185
|
+
### Added
|
|
186
|
+
- Initial release of the BeeL Node.js SDK
|
|
187
|
+
- Full TypeScript support with auto-generated types from OpenAPI spec
|
|
188
|
+
- Modular architecture with separate resource classes (Invoices, Customers, Products)
|
|
189
|
+
- HTTP client with automatic retry logic and exponential backoff
|
|
190
|
+
- Idempotency support for safe request retries
|
|
191
|
+
- Comprehensive error handling with custom error classes
|
|
192
|
+
- Support for all public API endpoints:
|
|
193
|
+
- Invoices (create, list, retrieve, update, delete, issue, send, mark as paid, download PDF)
|
|
194
|
+
- Customers (CRUD operations)
|
|
195
|
+
- Products (CRUD operations)
|
|
196
|
+
- Configuration (tax settings, VeriFactu, invoice series)
|
|
197
|
+
- NIF validation
|
|
198
|
+
- Business Profiles (multi-NIF support)
|
|
199
|
+
- Public metrics
|
|
200
|
+
|
|
201
|
+
### Configuration Options
|
|
202
|
+
- `apiKey` - Your BeeL API key (required)
|
|
203
|
+
- `baseUrl` - API base URL (default: `https://app.beel.es/api`)
|
|
204
|
+
- `timeout` - Request timeout in milliseconds (default: 30000)
|
|
205
|
+
- `maxRetries` - Maximum retry attempts for GET requests (default: 3)
|
|
206
|
+
- `retryDelay` - Initial retry delay in milliseconds (default: 1000)
|
|
207
|
+
|
|
208
|
+
### Error Types
|
|
209
|
+
- `BeelError` - Base error class
|
|
210
|
+
- `BeelAuthenticationError` - 401 authentication errors
|
|
211
|
+
- `BeelValidationError` - 400/422 validation errors
|
|
212
|
+
- `BeelNotFoundError` - 404 not found errors
|
|
213
|
+
- `BeelRateLimitError` - 429 rate limit errors
|
|
214
|
+
- `BeelTimeoutError` - Request timeout errors
|
|
215
|
+
- `BeelNetworkError` - Network/connection errors
|
|
216
|
+
|
|
217
|
+
## [1.0.0] - 2026-02-28
|
|
218
|
+
|
|
219
|
+
### Added
|
|
220
|
+
- Initial public release
|
|
221
|
+
- Complete SDK generated from OpenAPI specification
|
|
222
|
+
- TypeScript type definitions
|
|
223
|
+
- Comprehensive README with usage examples
|
|
224
|
+
- MIT License
|
|
225
|
+
|
|
226
|
+
[Unreleased]: https://github.com/beel-es/beel-node-sdk/compare/v1.0.0...HEAD
|
|
227
|
+
[1.0.0]: https://github.com/beel-es/beel-node-sdk/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -8,13 +8,17 @@ Official Node.js/TypeScript SDK for the [BeeL API](https://beel.es) — Spanish
|
|
|
8
8
|
|
|
9
9
|
**Node.js 18+. Single dependency ([openapi-fetch](https://openapi-ts.dev/openapi-fetch/)).**
|
|
10
10
|
|
|
11
|
+
**Reading this from `node_modules`?** Everything you need is in Markdown next to this file:
|
|
12
|
+
this README for common tasks, [`docs/reference/README.md`](docs/reference/README.md) for every
|
|
13
|
+
resource and method (HTTP operation, parameters, return shape, errors), and
|
|
14
|
+
[`llms.txt`](llms.txt) as a short index. You do not need to read `dist/index.d.ts`.
|
|
15
|
+
|
|
11
16
|
## Features
|
|
12
17
|
|
|
13
|
-
- **Full TypeScript types**
|
|
14
|
-
- **Automatic retries**
|
|
15
|
-
- **
|
|
16
|
-
- **Typed errors** — catch `BeeLNotFoundError` instead of checking status codes
|
|
17
|
-
- **PDF download** as Buffer
|
|
18
|
+
- **Full TypeScript types** generated from the OpenAPI contract, and a [typed Markdown reference](docs/reference/README.md)
|
|
19
|
+
- **Automatic retries** on 429, 5xx and network errors — only when repeating the request cannot apply it twice
|
|
20
|
+
- **Idempotency keys** on every POST, reused by that call's retries; `createOnce` for invoices you must never duplicate
|
|
21
|
+
- **Typed errors** — catch `BeeLNotFoundError` instead of checking status codes, read the API's `apiCode`
|
|
18
22
|
- **Webhook signature verification** with HMAC-SHA256
|
|
19
23
|
- **Instance-based client** — multiple API keys, no global state
|
|
20
24
|
- **ESM + CommonJS**
|
|
@@ -25,30 +29,204 @@ Official Node.js/TypeScript SDK for the [BeeL API](https://beel.es) — Spanish
|
|
|
25
29
|
npm install @beel_es/sdk
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
##
|
|
32
|
+
## Common tasks
|
|
33
|
+
|
|
34
|
+
Every example is typed: the request bodies are `components['schemas'][...]` of the contract,
|
|
35
|
+
which you can import with `import type { components } from '@beel_es/sdk'`.
|
|
36
|
+
|
|
37
|
+
### Set up the client
|
|
29
38
|
|
|
30
39
|
```typescript
|
|
31
40
|
import { BeeL } from '@beel_es/sdk';
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
// beel_sk_test_* → sandbox (VeriFactu test mode, no quota), beel_sk_live_* → production
|
|
43
|
+
const beel = new BeeL({ apiKey: process.env.BEEL_API_KEY! });
|
|
44
|
+
```
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
### Find the company id
|
|
47
|
+
|
|
48
|
+
Every invoice belongs to a company (a NIF), addressed by its UUID — not by the NIF itself.
|
|
49
|
+
Your API key tells you its account; the account lists its companies:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const { account_id } = await beel.catalogs.identity();
|
|
53
|
+
const { companies } = await beel.account(account_id).companies.list({ search: 'B12345678' });
|
|
54
|
+
|
|
55
|
+
const company = beel.company(companies[0].id!); // keep this id in your configuration
|
|
56
|
+
```
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
### Create and issue a standard invoice
|
|
59
|
+
|
|
60
|
+
A create makes a `DRAFT`; `issue` numbers it and submits it to VeriFactu. Or do both in one
|
|
61
|
+
call with `options.issue_directly`.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import type { components } from '@beel_es/sdk';
|
|
65
|
+
|
|
66
|
+
type CreateInvoiceRequest = components['schemas']['CreateInvoiceRequest'];
|
|
67
|
+
|
|
68
|
+
const body: CreateInvoiceRequest = {
|
|
40
69
|
type: 'STANDARD',
|
|
41
|
-
|
|
70
|
+
external_ref: 'ORD-2026-0042', // your order id: unique per live invoice, searchable
|
|
71
|
+
recipient: { customer_id: 'customer-uuid' }, // or the recipient's data inline
|
|
72
|
+
lines: [
|
|
73
|
+
{
|
|
74
|
+
description: 'Consulting',
|
|
75
|
+
quantity: 10,
|
|
76
|
+
unit_price: 85.5,
|
|
77
|
+
main_tax: { type: 'IVA', percentage: 21, regime_key: '01' }, // required on every line
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const draft = await company.invoices.create(body);
|
|
83
|
+
const issued = await company.invoices.issue(draft.id);
|
|
84
|
+
console.log(issued.invoice_number); // e.g. "A-2026/0001"
|
|
85
|
+
|
|
86
|
+
// Or in one call:
|
|
87
|
+
const invoice = await company.invoices.create({ ...body, options: { issue_directly: true } });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Create a simplified invoice (ticket)
|
|
91
|
+
|
|
92
|
+
For sales up to the legal limit without identifying the customer. The recipient may be empty
|
|
93
|
+
and must not carry a NIF.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const ticket = await company.invoices.create({
|
|
97
|
+
type: 'SIMPLIFIED',
|
|
98
|
+
external_ref: 'TICKET-2026-0815',
|
|
99
|
+
recipient: {},
|
|
42
100
|
lines: [
|
|
43
|
-
{
|
|
101
|
+
{ description: 'Menú del día', quantity: 2, unit_price: 14.5, main_tax: { type: 'IVA', percentage: 10, regime_key: '01' } },
|
|
44
102
|
],
|
|
103
|
+
options: { issue_directly: true },
|
|
45
104
|
});
|
|
105
|
+
```
|
|
46
106
|
|
|
47
|
-
|
|
107
|
+
If the customer later asks for a full invoice, exchange it:
|
|
108
|
+
`company.invoices.createSimplifiedExchange({ simplified_invoice_ids: [ticket.id], recipient: { customer_id } })`.
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
110
|
+
### Correct an issued invoice
|
|
111
|
+
|
|
112
|
+
An issued invoice is never edited. If the operation happened but the invoice is wrong, issue a
|
|
113
|
+
corrective; if it was issued by mistake, void it.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Partial: only the difference (here, a discount granted after the sale)
|
|
117
|
+
const corrective = await company.invoices.createCorrective(issued.id, {
|
|
118
|
+
rectification_type: 'PARTIAL',
|
|
119
|
+
rectification_code: 'R1',
|
|
120
|
+
reason: 'Discount agreed with the customer after the sale',
|
|
121
|
+
lines: [
|
|
122
|
+
{ description: 'Discount', quantity: -1, unit_price: 50, main_tax: { type: 'IVA', percentage: 21, regime_key: '01' } },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Total: cancels everything still invoiced on the original (no lines)
|
|
127
|
+
await company.invoices.createCorrective(issued.id, {
|
|
128
|
+
rectification_type: 'TOTAL',
|
|
129
|
+
rectification_code: 'R1',
|
|
130
|
+
reason: 'Order cancelled by agreement with the customer before delivery',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Issued by mistake (a test, an accidental duplicate):
|
|
134
|
+
await company.invoices.void(issued.id, { reason: 'Duplicate invoice issued by mistake', issued_in_error: true });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A corrective is not covered by the one-invoice-per-`external_ref` rule (it carries the order
|
|
138
|
+
reference of the invoice it corrects), so `createOnce` does not create correctives. To retry one
|
|
139
|
+
safely, list by `rectified_invoice_id` (`company.invoices.list({ rectified_invoice_id })`) before
|
|
140
|
+
sending it again, or pass the same `idempotencyKey` (see below).
|
|
141
|
+
|
|
142
|
+
### Find invoices by your own reference
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const { invoices } = await company.invoices.list({ external_ref: 'ORD-2026-0042' });
|
|
146
|
+
const invoice = invoices.find((i) => i.type === 'STANDARD' || i.type === 'SIMPLIFIED');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
At most one live (not deleted) standard or simplified invoice can hold a given `external_ref`
|
|
150
|
+
in each environment: a second create answers `409` with `apiCode`
|
|
151
|
+
`INVOICE_DUPLICATE_EXTERNAL_REFERENCE`.
|
|
152
|
+
|
|
153
|
+
### Read the VeriFactu status
|
|
154
|
+
|
|
155
|
+
AEAT answers asynchronously: a successful issue means *accepted for submission*.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const current = await company.invoices.get(invoiceId);
|
|
159
|
+
current.verifactu?.submission_status; // 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'VOIDED' | 'NOT_SUBMITTED'
|
|
160
|
+
current.verifactu?.qr_url; // AEAT verification URL, from submission on
|
|
161
|
+
current.verifactu?.error_code; // AEAT's code when it reported a problem
|
|
162
|
+
|
|
163
|
+
// The records themselves (registration, then cancellation if voided)
|
|
164
|
+
const records = await company.invoices.verifactuRecords(invoiceId);
|
|
165
|
+
|
|
166
|
+
// Everything AEAT rejected
|
|
167
|
+
const { invoices: rejected } = await company.invoices.list({ verifactu_status: 'REJECTED' });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
To be told instead of polling, subscribe a webhook to `verifactu.status.updated` (see
|
|
171
|
+
[Webhooks](#webhooks)).
|
|
172
|
+
|
|
173
|
+
### Retries, idempotency and never duplicating an invoice
|
|
174
|
+
|
|
175
|
+
What the client does on its own (defaults: `maxRetries: 3`, `autoIdempotencyKey: true`):
|
|
176
|
+
|
|
177
|
+
| Situation | Automatic retry? |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `429` rate limit | Yes, after `Retry-After`, any method. The API refused the request unprocessed. |
|
|
180
|
+
| `409 IDEMPOTENCY_KEY_PROCESSING` | Yes, with the same key: the first request is still running. |
|
|
181
|
+
| `5xx` or network error on a `GET` | Yes, with exponential backoff. |
|
|
182
|
+
| `5xx` or network error on a `POST` | Yes, **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. |
|
|
183
|
+
| `5xx` or network error on a `PUT`/`PATCH`/`DELETE`, or a `POST` sent without a key | No. |
|
|
184
|
+
| Any other `4xx` | No. |
|
|
185
|
+
|
|
186
|
+
- **One call, one key.** Every `POST` gets an `Idempotency-Key` (a UUID) once, and every
|
|
187
|
+
automatic retry of that call reuses it. An automatic retry never creates a second invoice.
|
|
188
|
+
- **A new call is a new key.** If `create()` finally throws a `5xx` or a network error, the
|
|
189
|
+
outcome is unknown: the invoice may exist. Calling `create()` again sends a new key and can
|
|
190
|
+
duplicate it. Do not do that.
|
|
191
|
+
- **The API stores the answer of a key for 24 hours** (a `2xx` or a `5xx`; a `4xx` is not
|
|
192
|
+
stored, so the corrected request can reuse the key). Retrying with the same key after a `5xx`
|
|
193
|
+
returns the same `5xx`.
|
|
194
|
+
|
|
195
|
+
The recommended pattern: give the invoice your order id as `external_ref`, and create it with
|
|
196
|
+
`createOnce`. It looks the reference up first, creates only if nothing holds it, and when the
|
|
197
|
+
create fails with an unknown outcome or a `409 INVOICE_DUPLICATE_EXTERNAL_REFERENCE`, it returns
|
|
198
|
+
the invoice that does exist. Calling it again after any error is safe.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const invoice = await company.invoices.createOnce({
|
|
202
|
+
type: 'STANDARD',
|
|
203
|
+
external_ref: order.id,
|
|
204
|
+
recipient: { customer_id: order.customerId },
|
|
205
|
+
lines: [{ description: 'Order', quantity: 1, unit_price: 100, main_tax: { type: 'IVA', percentage: 21, regime_key: '01' } }],
|
|
206
|
+
options: { issue_directly: true },
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
If you manage keys yourself, persist one per operation and pass it on every attempt:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
await company.invoices.create(body, undefined, { idempotencyKey: `order-${order.id}`, timeoutMs: 15_000 });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Errors keep the API's own code in `apiCode` (`code` stays the generic class code for backward
|
|
217
|
+
compatibility):
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { BeeLConflictError, BeeLErrorCodes } from '@beel_es/sdk';
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await company.invoices.create(body);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof BeeLConflictError && error.apiCode === BeeLErrorCodes.INVOICE_DUPLICATE_EXTERNAL_REFERENCE) {
|
|
226
|
+
// someone already created it: look it up by external_ref
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
52
230
|
```
|
|
53
231
|
|
|
54
232
|
## Companies (multi-NIF)
|
|
@@ -62,8 +240,22 @@ const company = beel.company('company-uuid');
|
|
|
62
240
|
// Invoices — full lifecycle
|
|
63
241
|
const { invoices, pagination } = await company.invoices.list({ status: 'ISSUED' });
|
|
64
242
|
await company.invoices.issue('invoice-uuid');
|
|
65
|
-
|
|
66
|
-
|
|
243
|
+
// A void is only for an invoice issued by mistake; `issued_in_error: true` is required
|
|
244
|
+
// once it has been sent or paid. An operation that did happen gets a corrective instead.
|
|
245
|
+
await company.invoices.void('invoice-uuid', { reason: 'Duplicate', issued_in_error: true });
|
|
246
|
+
await company.invoices.createCorrective('invoice-uuid', {
|
|
247
|
+
rectification_type: 'PARTIAL',
|
|
248
|
+
rectification_code: 'R1',
|
|
249
|
+
reason: 'Discount granted after the sale',
|
|
250
|
+
circumstance_date: '2026-09-01', // when the art. 80 circumstance happened, if any
|
|
251
|
+
lines: [ ... ],
|
|
252
|
+
});
|
|
253
|
+
// Exchange issued simplified invoices for a full one with the customer identified
|
|
254
|
+
await company.invoices.createSimplifiedExchange({
|
|
255
|
+
simplified_invoice_ids: ['simplified-uuid'],
|
|
256
|
+
recipient: { customer_id: 'customer-uuid' },
|
|
257
|
+
});
|
|
258
|
+
const records = await company.invoices.verifactuRecords('invoice-uuid'); // registration, cancellation
|
|
67
259
|
const pdf = await company.invoices.getPdf('invoice-uuid');
|
|
68
260
|
|
|
69
261
|
// Customers, products, series
|
|
@@ -134,7 +326,7 @@ wrapper is written by hand. `beel.raw` lets you call anything in the contract to
|
|
|
134
326
|
without waiting for a release:
|
|
135
327
|
|
|
136
328
|
```typescript
|
|
137
|
-
const { data } = await beel.raw.GET('/v1/companies/{company_id}/
|
|
329
|
+
const { data } = await beel.raw.GET('/v1/companies/{company_id}/invoice-customization', {
|
|
138
330
|
params: { path: { company_id: 'company-uuid' } },
|
|
139
331
|
});
|
|
140
332
|
```
|
|
@@ -143,6 +335,9 @@ It is the same client the resources use — authentication, retries and `Idempot
|
|
|
143
335
|
still apply — and it stays typed: a path, parameter or body the contract does not have
|
|
144
336
|
will not compile. What you give up is the sugar and the naming stability of the wrappers.
|
|
145
337
|
|
|
338
|
+
Error handling is the SDK's too: a non-2xx answer is **thrown** as a typed error (below), so a
|
|
339
|
+
`raw` call never resolves with openapi-fetch's `{ error }` — `data` is always the success body.
|
|
340
|
+
|
|
146
341
|
## Accounts (managed accounts, members & webhooks)
|
|
147
342
|
|
|
148
343
|
For integrators managing several accounts (provisioning, gestorías, platforms):
|
|
@@ -243,21 +438,27 @@ try {
|
|
|
243
438
|
|
|
244
439
|
| Error class | Status | When |
|
|
245
440
|
|---|---|---|
|
|
246
|
-
| `BeeLAuthError` | 401, 403 | Invalid or missing API key |
|
|
441
|
+
| `BeeLAuthError` | 401, 403 | Invalid or missing API key, or no access to the resource |
|
|
247
442
|
| `BeeLNotFoundError` | 404 | Resource doesn't exist |
|
|
248
|
-
| `BeeLValidationError` | 422 | Invalid data |
|
|
249
|
-
| `BeeLConflictError` | 409 | Duplicate (
|
|
250
|
-
| `BeeLRateLimitError` | 429 | Too many requests (
|
|
251
|
-
| `BeeLApiError` | 5xx |
|
|
443
|
+
| `BeeLValidationError` | 422 | Invalid data or a business rule |
|
|
444
|
+
| `BeeLConflictError` | 409 | Duplicate (`external_ref`, NIF…) or an `Idempotency-Key` conflict |
|
|
445
|
+
| `BeeLRateLimitError` | 429 | Too many requests (retried automatically first) |
|
|
446
|
+
| `BeeLApiError` | 400, 5xx | Bad request, or a server error (retried when safe) |
|
|
447
|
+
|
|
448
|
+
Every error carries `statusCode`, `apiCode` (the API's `error.code`, e.g.
|
|
449
|
+
`INVOICE_DUPLICATE_EXTERNAL_REFERENCE`), `code` (a generic code per class), `details` and
|
|
450
|
+
`requestId` — quote the last one when you contact support. See
|
|
451
|
+
[docs/reference/errors.md](docs/reference/errors.md).
|
|
252
452
|
|
|
253
453
|
## Automatic Retries
|
|
254
454
|
|
|
255
|
-
|
|
455
|
+
See [Retries, idempotency and never duplicating an invoice](#retries-idempotency-and-never-duplicating-an-invoice)
|
|
456
|
+
for exactly what is retried. Tune it with:
|
|
256
457
|
|
|
257
458
|
```typescript
|
|
258
459
|
const beel = new BeeL({
|
|
259
460
|
apiKey: 'beel_sk_live_...',
|
|
260
|
-
maxRetries: 5, // default: 3
|
|
461
|
+
maxRetries: 5, // default: 3 (0 disables retries)
|
|
261
462
|
retryDelayMs: 1000, // default: 500
|
|
262
463
|
maxRetryDelayMs: 60000, // default: 30000
|
|
263
464
|
});
|
|
@@ -290,8 +491,11 @@ app.post('/webhooks/beel', express.raw({ type: 'application/json' }), (req, res)
|
|
|
290
491
|
req.headers['beel-signature'] as string,
|
|
291
492
|
);
|
|
292
493
|
|
|
494
|
+
// "Send test" deliveries carry a synthetic `data`: acknowledge and stop.
|
|
495
|
+
if (event.test) return res.status(200).send('OK');
|
|
496
|
+
|
|
293
497
|
if (event.type === 'verifactu.status.updated') {
|
|
294
|
-
console.log(event.data.new_status); // 'ACCEPTED' | 'REJECTED'
|
|
498
|
+
console.log(event.data.new_status); // 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'VOIDED'
|
|
295
499
|
}
|
|
296
500
|
|
|
297
501
|
res.status(200).send('OK');
|
|
@@ -317,6 +521,7 @@ const customer = CustomerBuilder.create()
|
|
|
317
521
|
|
|
318
522
|
const invoice = InvoiceBuilder.create()
|
|
319
523
|
.forCustomer('customer-uuid')
|
|
524
|
+
.mainTax({ type: 'IVA', percentage: 21, regime_key: '01' }) // required on every line
|
|
320
525
|
.addLine('Service A', 5, 100.0)
|
|
321
526
|
.addLine('Service B', 3, 150.0)
|
|
322
527
|
.metadata({ stripe_id: 'pi_abc123' })
|
|
@@ -342,12 +547,15 @@ const beel = new BeeL({
|
|
|
342
547
|
maxRetries: 3, // Default
|
|
343
548
|
retryDelayMs: 500, // Default
|
|
344
549
|
maxRetryDelayMs: 30000, // Default
|
|
550
|
+
autoIdempotencyKey: true, // Default
|
|
345
551
|
});
|
|
346
552
|
```
|
|
347
553
|
|
|
348
554
|
## Documentation
|
|
349
555
|
|
|
350
|
-
|
|
556
|
+
- SDK reference: [docs/reference/README.md](docs/reference/README.md) — every resource and
|
|
557
|
+
method, generated from the source and the contract (`npm run docs:reference`).
|
|
558
|
+
- API docs: [docs.beel.es](https://docs.beel.es)
|
|
351
559
|
|
|
352
560
|
## License
|
|
353
561
|
|