@beel_es/sdk 2.0.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +216 -25
  3. package/dist/index.d.mts +1450 -181
  4. package/dist/index.d.ts +1450 -181
  5. package/dist/index.js +1411 -188
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1409 -188
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/reference/README.md +233 -0
  10. package/docs/reference/account-companies.md +55 -0
  11. package/docs/reference/account-invitations.md +57 -0
  12. package/docs/reference/account-members.md +97 -0
  13. package/docs/reference/account-webhooks.md +123 -0
  14. package/docs/reference/account.md +94 -0
  15. package/docs/reference/accounts.md +72 -0
  16. package/docs/reference/catalogs.md +68 -0
  17. package/docs/reference/client.md +113 -0
  18. package/docs/reference/company-customers.md +85 -0
  19. package/docs/reference/company-invoices-schedule.md +52 -0
  20. package/docs/reference/company-invoices.md +316 -0
  21. package/docs/reference/company-payment-connections-events.md +103 -0
  22. package/docs/reference/company-payment-connections.md +87 -0
  23. package/docs/reference/company-products.md +72 -0
  24. package/docs/reference/company-recurring-invoices.md +136 -0
  25. package/docs/reference/company-series.md +94 -0
  26. package/docs/reference/company-tax-configuration.md +36 -0
  27. package/docs/reference/company-verifactu-configuration.md +41 -0
  28. package/docs/reference/company.md +94 -0
  29. package/docs/reference/configuration.md +90 -0
  30. package/docs/reference/customer-builder.md +107 -0
  31. package/docs/reference/customers.md +91 -0
  32. package/docs/reference/errors.md +72 -0
  33. package/docs/reference/invoice-builder.md +149 -0
  34. package/docs/reference/invoices.md +250 -0
  35. package/docs/reference/nif.md +31 -0
  36. package/docs/reference/products.md +111 -0
  37. package/docs/reference/series.md +81 -0
  38. package/docs/reference/types.md +222 -0
  39. package/docs/reference/webhook-verifier.md +95 -0
  40. package/llms.txt +52 -0
  41. package/package.json +10 -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** auto-generated from OpenAPI spec
14
- - **Automatic retries** with exponential backoff on 429 and 5xx
15
- - **Auto idempotency keys** on every POST request
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
- ## Quick Start
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
- const beel = new BeeL({ apiKey: 'beel_sk_live_...' });
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
- // Address a company (NIF) explicitly — the recommended multi-NIF surface
36
- const company = beel.company('company-uuid');
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
- // Create a draft invoice
39
- const invoice = await company.invoices.create({
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
- recipient: { customer_id: 'customer-uuid' },
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
- { line_type: 'NORMAL', description: 'Consulting', quantity: 1, unit_price: 100, discount_percentage: 0 },
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
- console.log(invoice.id); // UUID assigned immediately
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
- // Issue it to assign an invoice number and trigger VeriFactu
50
- const issued = await company.invoices.issue(invoice.id);
51
- console.log(issued.invoice_number); // e.g. "A-2026/0001"
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)
@@ -157,6 +335,9 @@ It is the same client the resources use — authentication, retries and `Idempot
157
335
  still apply — and it stays typed: a path, parameter or body the contract does not have
158
336
  will not compile. What you give up is the sugar and the naming stability of the wrappers.
159
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
+
160
341
  ## Accounts (managed accounts, members & webhooks)
161
342
 
162
343
  For integrators managing several accounts (provisioning, gestorías, platforms):
@@ -257,21 +438,27 @@ try {
257
438
 
258
439
  | Error class | Status | When |
259
440
  |---|---|---|
260
- | `BeeLAuthError` | 401, 403 | Invalid or missing API key |
441
+ | `BeeLAuthError` | 401, 403 | Invalid or missing API key, or no access to the resource |
261
442
  | `BeeLNotFoundError` | 404 | Resource doesn't exist |
262
- | `BeeLValidationError` | 422 | Invalid data |
263
- | `BeeLConflictError` | 409 | Duplicate (e.g. same NIF) |
264
- | `BeeLRateLimitError` | 429 | Too many requests (auto-retried) |
265
- | `BeeLApiError` | 5xx | Server error (auto-retried) |
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).
266
452
 
267
453
  ## Automatic Retries
268
454
 
269
- 429 and 5xx errors are retried with exponential backoff. Client errors (400, 401, 404, 422) are never retried.
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:
270
457
 
271
458
  ```typescript
272
459
  const beel = new BeeL({
273
460
  apiKey: 'beel_sk_live_...',
274
- maxRetries: 5, // default: 3
461
+ maxRetries: 5, // default: 3 (0 disables retries)
275
462
  retryDelayMs: 1000, // default: 500
276
463
  maxRetryDelayMs: 60000, // default: 30000
277
464
  });
@@ -334,6 +521,7 @@ const customer = CustomerBuilder.create()
334
521
 
335
522
  const invoice = InvoiceBuilder.create()
336
523
  .forCustomer('customer-uuid')
524
+ .mainTax({ type: 'IVA', percentage: 21, regime_key: '01' }) // required on every line
337
525
  .addLine('Service A', 5, 100.0)
338
526
  .addLine('Service B', 3, 150.0)
339
527
  .metadata({ stripe_id: 'pi_abc123' })
@@ -359,12 +547,15 @@ const beel = new BeeL({
359
547
  maxRetries: 3, // Default
360
548
  retryDelayMs: 500, // Default
361
549
  maxRetryDelayMs: 30000, // Default
550
+ autoIdempotencyKey: true, // Default
362
551
  });
363
552
  ```
364
553
 
365
554
  ## Documentation
366
555
 
367
- Full API docs: [docs.beel.es](https://docs.beel.es)
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)
368
559
 
369
560
  ## License
370
561