@taxora/sdk 1.8.0 → 1.10.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 +97 -0
- package/README.md +239 -4
- package/dist/index.cjs +1382 -242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +748 -63
- package/dist/index.d.ts +748 -63
- package/dist/index.js +1369 -242
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,103 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@taxora/sdk` are documented here. Versions follow [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.10.0] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `ComplianceTax.rate()`, `zeroRated()`, `exempt()` and `reverseCharge()` build line taxes with the right category.
|
|
12
|
+
- `calculateInvoiceTotals(lines, { expectedTotal })` checks your own total against the lines and reports `roundingDifference`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `createTransaction()` refuses impossible input before the request: an unknown tax category, a category `E` tax
|
|
17
|
+
without its exemption code (`comment`), a percent outside 0–100, a currency or country code that is not letters
|
|
18
|
+
(currency, counterparty country and tax categories are upper-cased), a due date before the invoice date, a credit
|
|
19
|
+
note without `amendedNumber` (or `amendedNumber` without `isCreditNote`), a credit note reusing the number it corrects
|
|
20
|
+
and a corrected invoice dated after the credit note.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`eReporting.createCreditNote(original, input)`** — credit an invoice you sent through Taxora. Buyer, lines, amounts,
|
|
25
|
+
currency and the reference to the corrected invoice (number and date) are taken from `original`; partial credits pass
|
|
26
|
+
`invoiceLines`, `subtotal`, `taxAmount` and `total` together. Refused up front for invoices that were never sent
|
|
27
|
+
(`not_required`) and for credit notes.
|
|
28
|
+
- **`eReporting.decideDelivery(country, id, scheme?)`** — `peppolLookup()` turned into an action
|
|
29
|
+
(`ComplianceDeliveryAction`: `einvoice`, `einvoice_pending`, `regular_invoice`) for a shop checkout.
|
|
30
|
+
- **`calculateInvoiceTotals(lines)`** — subtotal, VAT and total of invoice lines, rounded the way the e-invoice is built,
|
|
31
|
+
so the header amounts of `createTransaction()` always match the lines.
|
|
32
|
+
- **`ComplianceTaxCategory`** (`S`, `AA`, `AAA`, `K`, `G`, `AE`, `E`, `Z`) and `parseComplianceTaxCategory()`.
|
|
33
|
+
- **`billing.getEInvoicingQuota(months?)`** — `GET /billing/einvoicing-quota` as `EInvoicingQuota` (documents included,
|
|
34
|
+
used, still free and what is due per month); `null` without e-invoicing.
|
|
35
|
+
|
|
36
|
+
## [1.9.0] — 2026-10-27
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`ComplianceCoverageCountry.support`** — `'yes' | 'cross_border_only' | 'coming_soon'` from
|
|
41
|
+
`GET /compliance/offerings`: whether a country is fully served, reached only for invoices across the border, or not
|
|
42
|
+
live yet (can still be requested). Defaults to `'yes'` for older API versions.
|
|
43
|
+
- **`eReporting.getNotificationSettings()` / `eReporting.updateNotificationSettings(input)`** —
|
|
44
|
+
`GET` / `PUT /compliance/notification-settings`: the company's opt-in compliance emails as
|
|
45
|
+
`ComplianceNotificationSettings` (`{ recipients, language, notOnPeppol, digest, digestSentAt }`). `notOnPeppol` =
|
|
46
|
+
hourly email listing invoices held back because the buyer is not on Peppol; `digest` = `'off' | 'daily' | 'weekly'`
|
|
47
|
+
summary. Updates send only the provided fields (`UpdateComplianceNotificationSettingsInput`, at least one; max 5
|
|
48
|
+
recipients, validated before the request); 422 when an email is switched on without recipients, 403 `admin_only` /
|
|
49
|
+
`read_only_role` for non-admins.
|
|
50
|
+
- **`eReporting.exportTransactionsCsv(filters)`** — `GET /compliance/transactions/export.csv`: all transactions
|
|
51
|
+
matching the `listTransactions()` filters (without pagination, type `ComplianceTransactionExportFilters`) as a
|
|
52
|
+
semicolon-separated CSV for bookkeeping, incl. internal reference, delivery state, buyer rejection and ECB EUR
|
|
53
|
+
columns (max 50,000 rows). Returned verbatim as a string.
|
|
54
|
+
- **`ComplianceTransaction.eurEquivalent`** (`{ subtotal, taxAmount, total, rate, rateDate, source: 'ECB' }`): the
|
|
55
|
+
amounts of a non-EUR invoice in EUR at the ECB reference rate of the invoice date; null for EUR invoices or when no
|
|
56
|
+
rate is known.
|
|
57
|
+
- **E-invoicing status signals:** `compliance.transaction.state_changed` now fires for every real change (Taxora's
|
|
58
|
+
own state and the provider's delivery state: accepted, refused, paid, …; never twice, never for `sending`).
|
|
59
|
+
`ComplianceTransactionWebhookObject` gained the optional `internal_reference`, `previous_state`,
|
|
60
|
+
`previous_provider_state`, `buyer_rejected`, `rejection_reason`, `submission_error` and `delivery_check`, plus the
|
|
61
|
+
helper **`isTransactionBuyerRejected(object)`**.
|
|
62
|
+
- **`ComplianceTransaction.buyerRejected` / `.rejectionReason`** and the **`providerState`** filter on
|
|
63
|
+
`listTransactions()` (e.g. `'refused'` = rejected by the buyer).
|
|
64
|
+
- **`eReporting.getTransactionPdf(id)`** downloads the invoice as a readable PDF (the attached seller PDF or the
|
|
65
|
+
provider's rendering of the e-invoice) as `{ content, contentType, filename }`; 409 while no PDF is available.
|
|
66
|
+
**`ComplianceTransaction.pdfUrl`** is set once a PDF is available.
|
|
67
|
+
- **Internal reference:** `internalReference` on `createTransaction()` and `updateTransaction()` (max 100 chars;
|
|
68
|
+
an empty string clears it on update) and `ComplianceTransaction.internalReference`. Your own reference (order
|
|
69
|
+
number, ERP id) for searching — never sent to the buyer or the tax authority. Also an optional CSV import column
|
|
70
|
+
`internal_reference`.
|
|
71
|
+
- **`listTransactions()` filters `search`** (contains-match over invoice number, internal reference and buyer name)
|
|
72
|
+
**and `internalReference`** (exact match), both max 100 chars.
|
|
73
|
+
- **`eReporting.getTransactionDocument(id)`** downloads the legal e-invoice of a sent transaction
|
|
74
|
+
(`{ content: Uint8Array, contentType, filename }`); 409 while it is not available yet.
|
|
75
|
+
- **Invoice PDF attachment:** `invoicePdf` / `invoicePdfFilename` on `createTransaction()` and `updateTransaction()`
|
|
76
|
+
(e-invoicing only). Bytes (`Buffer` / `Uint8Array` / `ArrayBuffer`) are checked (PDF, max 10 MB) and base64-encoded;
|
|
77
|
+
strings are sent as-is (already base64).
|
|
78
|
+
- **`ComplianceTransaction.documentUrl`, `.deliveryCheck` and `.invoicePdf`** (`{ filename, size, attached }`).
|
|
79
|
+
- **`ComplianceTransactionState.NOT_REQUIRED`** (`not_required`): the buyer cannot receive e-invoices, so nothing was
|
|
80
|
+
sent or billed.
|
|
81
|
+
- **`eReporting.exportRevenueTaxRatesCsv(filters)`**: net and VAT per month and tax rate as CSV (BOM preserved).
|
|
82
|
+
- **Self-service plans:** `eReporting.getOfferings()` (`ComplianceOfferings`), `createCheckout(country, plan)`,
|
|
83
|
+
`confirmCheckout(sessionId)` and `createBillingPortalSession()`.
|
|
84
|
+
- **`client.billing`** (`BillingEndpoint`): `listUsageSummaries(page, perPage)` (`UsageSummaryPage`) and
|
|
85
|
+
`getUsageSummaries(period)` (`UsageSummary[]` with events).
|
|
86
|
+
- **Per-country filters:** `complianceEnrollmentId` on `getRevenueStatistics()` and `exportRevenueTaxRatesCsv()`;
|
|
87
|
+
`country` as 4th argument of `listTaxReports(page, perPage, state, country)`.
|
|
88
|
+
- README: compliance write requests by a read-only user return 403 with code `read_only_role` (HttpException).
|
|
89
|
+
- README: compliance webhook events (`compliance.transaction.state_changed`, `compliance.ledger.state_changed`) listed
|
|
90
|
+
with their payloads.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **`ComplianceOfferingPlan.requires`** is now typed `{ country: string; service: string } | null` and parsed from the
|
|
95
|
+
object the API sends (e.g. `{ country: 'FR', service: 'e_reporting' }`). It was typed `unknown[] | null` and always
|
|
96
|
+
came back `null`. Public type change.
|
|
97
|
+
- `createTransaction()` / `updateTransaction()` reject `invoicePdfFilename` without `invoicePdf`
|
|
98
|
+
(`invoicePdfFilename requires invoicePdf.`), like the PHP SDK.
|
|
99
|
+
- README: `admin_only` (403) listed with the compliance error codes; the webhook example no longer mentions a
|
|
100
|
+
`not_required` state event (it is never sent).
|
|
101
|
+
|
|
5
102
|
## [1.8.0] — 2026-10-25
|
|
6
103
|
|
|
7
104
|
### Added
|
package/README.md
CHANGED
|
@@ -124,7 +124,10 @@ TaxoraClientFactory.create()
|
|
|
124
124
|
└── TaxoraClient
|
|
125
125
|
├── auth → AuthEndpoint (login, loginWithClientId, refresh)
|
|
126
126
|
├── vat → VatEndpoint (validate, history, search, certificates)
|
|
127
|
-
|
|
127
|
+
├── company → CompanyEndpoint (company info)
|
|
128
|
+
├── smartEnrichment → SmartEnrichmentEndpoint (reverse VAT lookup)
|
|
129
|
+
├── eReporting → EReportingEndpoint (compliance: enrollments, transactions, plans)
|
|
130
|
+
└── billing → BillingEndpoint (usage summaries)
|
|
128
131
|
```
|
|
129
132
|
|
|
130
133
|
Each endpoint handles:
|
|
@@ -153,6 +156,8 @@ Each endpoint handles:
|
|
|
153
156
|
| `ProviderDocument` | Attached provider document metadata (type, date, MIME, hash, nested line item) |
|
|
154
157
|
| `ComplianceEnrollment` | Per-country compliance enrollment: country, regime, `service`, provisioning status |
|
|
155
158
|
| `ComplianceTransaction` | Recorded transaction / e-invoice: amounts, state, buyer fields, `documentType`, `providerState` |
|
|
159
|
+
| `ComplianceOfferings` | Bookable compliance plans per country, coverage, unlocked services |
|
|
160
|
+
| `UsageSummary` | Billed usage of one feature in one billing period (YYYY-MM), optionally with its events |
|
|
156
161
|
| `ComplianceTaxReport` | DGFiP tax report lifecycle of a transaction (France) |
|
|
157
162
|
| `SireneLookupResult` | Company data from the French SIRENE registry |
|
|
158
163
|
| `RegistryCompany` | Company from a national register (Norway: Brønnøysund) — org number, VAT / register flags |
|
|
@@ -626,6 +631,7 @@ const tx = await client.eReporting.createTransaction({
|
|
|
626
631
|
taxes: [{ name: 'TVA', percent: 20, category: 'S' }], // category E needs a VATEX `comment`
|
|
627
632
|
},
|
|
628
633
|
],
|
|
634
|
+
internalReference: 'ORDER-4711', // optional, max 100: your own reference (order no., ERP id) — never sent to the buyer or the tax authority
|
|
629
635
|
submitNow: false, // record now, submit later
|
|
630
636
|
});
|
|
631
637
|
|
|
@@ -637,15 +643,34 @@ console.log(submitted.state, submitted.stateLabel); // e.g. "submitted"
|
|
|
637
643
|
const transactions = await client.eReporting.listTransactions({
|
|
638
644
|
dateFrom: '2026-07-01', // or a Date
|
|
639
645
|
dateTo: '2026-07-31',
|
|
640
|
-
state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error'
|
|
646
|
+
state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error' | 'not_required'
|
|
641
647
|
transactionType: 'b2c_outbound',
|
|
642
648
|
complianceEnrollmentId: enrollment.id, // optional: one enrollment only
|
|
643
649
|
country: 'FR', // optional: one country only
|
|
650
|
+
search: 'ACME', // optional: contains-match over invoice number, internal reference and buyer name
|
|
651
|
+
internalReference: 'ORDER-4711', // optional: exact match on your internal reference
|
|
652
|
+
providerState: 'refused', // optional: provider delivery state, 'refused' = rejected by the buyer
|
|
644
653
|
page: 1,
|
|
645
654
|
perPage: 25,
|
|
646
655
|
});
|
|
647
656
|
const one = await client.eReporting.getTransaction(tx.id);
|
|
657
|
+
// Non-EUR invoices carry the EUR amounts at the ECB reference rate of the invoice date
|
|
658
|
+
// (null for EUR invoices or when no rate is known):
|
|
659
|
+
if (one.eurEquivalent) {
|
|
660
|
+
const { total, rate, rateDate, source } = one.eurEquivalent; // decimal strings, e.g. '98.92', '11.6250'
|
|
661
|
+
console.log(`${total} EUR @ ${rate} (${source}, ${rateDate})`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// The same filters (without page/perPage) as a semicolon-separated CSV for your bookkeeping —
|
|
665
|
+
// incl. internal reference, delivery state, buyer rejection and the eur_rate / eur_rate_date /
|
|
666
|
+
// eur_subtotal / eur_tax_amount / eur_total columns. Max 50,000 rows; returned verbatim.
|
|
667
|
+
const bookkeepingCsv = await client.eReporting.exportTransactionsCsv({
|
|
668
|
+
dateFrom: '2026-01-01',
|
|
669
|
+
dateTo: '2026-09-30',
|
|
670
|
+
country: 'NO',
|
|
671
|
+
});
|
|
648
672
|
await client.eReporting.updateTransaction(tx.id, { total: 130, taxAmount: 21.67 });
|
|
673
|
+
await client.eReporting.updateTransaction(tx.id, { internalReference: '' }); // empty string clears it
|
|
649
674
|
await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
|
|
650
675
|
```
|
|
651
676
|
|
|
@@ -653,7 +678,8 @@ await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
|
|
|
653
678
|
|
|
654
679
|
Bulk-import from a semicolon-separated CSV (one row per invoice line; rows sharing an
|
|
655
680
|
`invoice_number` are grouped into one transaction). Idempotent: re-uploading skips rows whose
|
|
656
|
-
invoice already exists.
|
|
681
|
+
invoice already exists. An optional `internal_reference` column sets your own reference per
|
|
682
|
+
invoice.
|
|
657
683
|
|
|
658
684
|
```ts
|
|
659
685
|
import { readFileSync } from 'fs';
|
|
@@ -674,7 +700,7 @@ Every submitted transaction is tracked through the aggregated DGFiP ledger lifec
|
|
|
674
700
|
(`new → sent → acknowledged → registered` or `refused`/`error`).
|
|
675
701
|
|
|
676
702
|
```ts
|
|
677
|
-
const reports = await client.eReporting.listTaxReports(1, 25, 'refused'); // state
|
|
703
|
+
const reports = await client.eReporting.listTaxReports(1, 25, 'refused', 'FR'); // state + country filters optional
|
|
678
704
|
for (const report of reports) {
|
|
679
705
|
console.log(report.state, report.stateLabel, report.isTerminal, report.refusalReason);
|
|
680
706
|
}
|
|
@@ -712,6 +738,7 @@ const stats = await client.eReporting.getRevenueStatistics({
|
|
|
712
738
|
interval: 'month', // 'day' | 'week' | 'month' (default 'month')
|
|
713
739
|
transactionType: 'b2c_outbound', // optional filter
|
|
714
740
|
state: 'submitted', // optional filter
|
|
741
|
+
complianceEnrollmentId: enrollment.id, // optional: one enrollment = one country's figures
|
|
715
742
|
});
|
|
716
743
|
|
|
717
744
|
console.log(stats.primaryCurrency); // e.g. EUR
|
|
@@ -724,6 +751,23 @@ for (const row of stats.byCountry) {
|
|
|
724
751
|
}
|
|
725
752
|
```
|
|
726
753
|
|
|
754
|
+
### Tax-rate CSV
|
|
755
|
+
|
|
756
|
+
Net and VAT per month, currency and tax rate (semicolon-separated, computed from the invoice lines) — for reconciling
|
|
757
|
+
the reported figures with your VAT return. Same filters as `getRevenueStatistics()` without `interval`. The body is
|
|
758
|
+
returned byte-for-byte, a UTF-8 BOM included.
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
import { writeFileSync } from 'fs';
|
|
762
|
+
|
|
763
|
+
const csv = await client.eReporting.exportRevenueTaxRatesCsv({
|
|
764
|
+
dateFrom: '2026-01-01',
|
|
765
|
+
dateTo: '2026-12-31',
|
|
766
|
+
complianceEnrollmentId: enrollment.id, // optional: one country only
|
|
767
|
+
});
|
|
768
|
+
writeFileSync('tax-rates.csv', csv);
|
|
769
|
+
```
|
|
770
|
+
|
|
727
771
|
### 🇳🇴 E-Invoicing Norway (EHF / Peppol)
|
|
728
772
|
|
|
729
773
|
Send EHF e-invoices and credit notes (Peppol BIS Billing 3.0) to Norwegian buyers. The
|
|
@@ -777,6 +821,41 @@ if (reach.isPending) {
|
|
|
777
821
|
console.log(reach.status, reach.reachable, reach.documentTypes); // 'reachable' true [...]
|
|
778
822
|
```
|
|
779
823
|
|
|
824
|
+
Or let the SDK turn the answer into an action (shop checkout, one call):
|
|
825
|
+
|
|
826
|
+
```ts
|
|
827
|
+
const decision = await client.eReporting.decideDelivery('NO', '974760673');
|
|
828
|
+
switch (decision.action) {
|
|
829
|
+
case 'einvoice': // create the transaction, Taxora sends the e-invoice
|
|
830
|
+
case 'einvoice_pending': // no answer yet: create it anyway, Taxora asks again before sending
|
|
831
|
+
break;
|
|
832
|
+
case 'regular_invoice': // not on Peppol: send your usual invoice, nothing is billed
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
The SDK refuses input that cannot be right before any request: unknown tax categories
|
|
838
|
+
(`ComplianceTaxCategory`: `S`, `AA`, `AAA`, `K`, `G`, `AE`, `E`, `Z`, any case), a category `E` line without its VATEX
|
|
839
|
+
exemption code, currency and country codes that are not letters (they are upper-cased), a due date before the invoice
|
|
840
|
+
date, and credit notes without (or invoices with) a corrected invoice. The API additionally checks that an e-invoice's
|
|
841
|
+
`subtotal`, `taxAmount` and `total` match its lines — compute them from the lines:
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
import { calculateInvoiceTotals } from '@taxora/sdk';
|
|
845
|
+
|
|
846
|
+
const { subtotal, taxAmount, total } = calculateInvoiceTotals(invoiceLines); // rounded like the e-invoice
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
Taxes without remembering category codes: `ComplianceTax.rate(25)` (category `S`, any rate above 0 %; in Norway 25/15/12 % are all `S`, for France's reduced rates pass `ComplianceTax.rate(10, 'TVA', 'AA')`), `zeroRated()` (`Z`),
|
|
850
|
+
`exempt('VATEX-EU-132')` (`E`, code mandatory) and `reverseCharge()` (`AE`). The rate itself is yours to know; the API checks
|
|
851
|
+
it against the country's catalogue. The tax name defaults to `VAT` (`ComplianceTax.rate(25, 'MVA')`).
|
|
852
|
+
|
|
853
|
+
If your own system already shows the buyer a total, pass it as a safety net:
|
|
854
|
+
`calculateInvoiceTotals(lines, { expectedTotal: 1249.99 })`. The totals always follow the lines, because the e-invoice is built
|
|
855
|
+
from them. A difference up to 0.05 is reported as `roundingDifference`; anything larger throws, since then quantities, prices or
|
|
856
|
+
rates are wrong. The invoice number is printed on the buyer's e-invoice and must be unique per enrollment (a different invoice
|
|
857
|
+
with a used number answers `409 invoice_number_taken`).
|
|
858
|
+
|
|
780
859
|
**4. Send an invoice** — Norway requires `transactionType: 'b2b_domestic_outbound'`, the buyer's
|
|
781
860
|
organisation number (`counterpartyRegisterId`) and `counterpartyName` (enforced server-side, 422):
|
|
782
861
|
|
|
@@ -833,10 +912,122 @@ const creditNote = await client.eReporting.createTransaction({
|
|
|
833
912
|
});
|
|
834
913
|
console.log(creditNote.isCreditNote); // true
|
|
835
914
|
|
|
915
|
+
// Shorter and harder to get wrong: credit an invoice you already sent. Buyer, lines, amounts,
|
|
916
|
+
// currency and the reference to the corrected invoice are taken from it.
|
|
917
|
+
const credit = await client.eReporting.createCreditNote(invoice, { creditNoteNumber: 'NO-2026-0001-CN' });
|
|
918
|
+
|
|
919
|
+
// Partial credit: pass the credited lines and all amounts together.
|
|
920
|
+
await client.eReporting.createCreditNote(invoice, {
|
|
921
|
+
creditNoteNumber: 'NO-2026-0001-CN2',
|
|
922
|
+
invoiceLines: [
|
|
923
|
+
{ description: 'Consulting', quantity: 1, price: 1000, taxes: [{ name: 'MVA', percent: 25, category: 'S' }] },
|
|
924
|
+
],
|
|
925
|
+
subtotal: 1000,
|
|
926
|
+
taxAmount: 250,
|
|
927
|
+
total: 1250,
|
|
928
|
+
});
|
|
929
|
+
|
|
836
930
|
// All Norwegian documents
|
|
837
931
|
const noDocs = await client.eReporting.listTransactions({ country: 'NO' });
|
|
838
932
|
```
|
|
839
933
|
|
|
934
|
+
**6. Attach your own invoice PDF** (e-invoicing only, max 10 MB) — pass the bytes (`Buffer` / `Uint8Array` /
|
|
935
|
+
`ArrayBuffer`); the SDK base64-encodes them. A string is sent as-is and must already be base64. The structured
|
|
936
|
+
e-invoice stays the legal document; whether the buyer's system shows the PDF depends on that system.
|
|
937
|
+
|
|
938
|
+
```ts
|
|
939
|
+
import { readFileSync } from 'fs';
|
|
940
|
+
|
|
941
|
+
const withPdf = await client.eReporting.createTransaction({
|
|
942
|
+
...invoiceInput,
|
|
943
|
+
invoicePdf: readFileSync('NO-2026-0002.pdf'),
|
|
944
|
+
invoicePdfFilename: 'NO-2026-0002.pdf', // optional, defaults to the invoice number
|
|
945
|
+
});
|
|
946
|
+
console.log(withPdf.invoicePdf); // { filename, size, attached } — attached once uploaded with the e-invoice
|
|
947
|
+
|
|
948
|
+
await client.eReporting.updateTransaction(withPdf.id, { invoicePdf: readFileSync('fixed.pdf') }); // replaces it
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
**7. Buyer not on Peppol / download the legal document**
|
|
952
|
+
|
|
953
|
+
```ts
|
|
954
|
+
const tx = await client.eReporting.getTransaction(invoice.id);
|
|
955
|
+
|
|
956
|
+
if (tx.state === 'not_required') {
|
|
957
|
+
// The buyer cannot receive e-invoices: nothing was sent or billed — send your regular invoice.
|
|
958
|
+
console.log(tx.deliveryCheck); // Peppol directory answer, e.g. { status: 'not_reachable', … }
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (tx.documentUrl !== null) {
|
|
962
|
+
const doc = await client.eReporting.getTransactionDocument(tx.id); // 409 while not available yet
|
|
963
|
+
writeFileSync(doc.filename ?? `${tx.invoiceNumber}.xml`, doc.content); // doc.contentType e.g. application/xml
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// The invoice as a readable PDF (your attached PDF, or the provider's rendering of the e-invoice)
|
|
967
|
+
if (tx.pdfUrl !== null) {
|
|
968
|
+
const pdf = await client.eReporting.getTransactionPdf(tx.id); // 409 while no PDF is available
|
|
969
|
+
writeFileSync(pdf.filename ?? `${tx.invoiceNumber}.pdf`, pdf.content); // pdf.contentType: application/pdf
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### Self-service plans (Stripe)
|
|
974
|
+
|
|
975
|
+
Book compliance plans without contacting sales. These routes work for companies **without** compliance access.
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
const { offerings, coverage, access, accessByCountry } = await client.eReporting.getOfferings();
|
|
979
|
+
const plan = offerings.find((o) => o.country === 'NO')?.plans.find((p) => p.checkoutAvailable);
|
|
980
|
+
|
|
981
|
+
const session = await client.eReporting.createCheckout('NO', plan!.key); // 422 if contact-only / already active
|
|
982
|
+
// open session.url in the browser; Stripe returns the user to the Taxora portal
|
|
983
|
+
const state = await client.eReporting.confirmCheckout(session.id); // idempotent → { active, country, service, plan, status }
|
|
984
|
+
|
|
985
|
+
const portalUrl = await client.eReporting.createBillingPortalSession(); // invoices, payment method, cancel; 404 without a Stripe plan
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### Notification emails
|
|
989
|
+
|
|
990
|
+
Opt-in e-reporting / e-invoicing emails for your company — readable by every member, changeable by company **admins**
|
|
991
|
+
only.
|
|
992
|
+
|
|
993
|
+
```ts
|
|
994
|
+
const settings = await client.eReporting.getNotificationSettings();
|
|
995
|
+
// { recipients: ['accounting@example.com'], language: 'de' | 'en', notOnPeppol: boolean,
|
|
996
|
+
// digest: 'off' | 'daily' | 'weekly', digestSentAt: string | null }
|
|
997
|
+
|
|
998
|
+
// Only the provided fields change (at least one is required); recipients replaces the whole list (max 5):
|
|
999
|
+
await client.eReporting.updateNotificationSettings({
|
|
1000
|
+
recipients: ['accounting@example.com', 'cfo@example.com'],
|
|
1001
|
+
language: 'en',
|
|
1002
|
+
notOnPeppol: true, // hourly email listing invoices held back because the buyer is not on Peppol
|
|
1003
|
+
digest: 'weekly', // 'daily' = 08:00 for the day before, 'weekly' = Mondays for the last 7 days
|
|
1004
|
+
});
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
Invalid input (more than 5 or invalid addresses, unknown `language` / `digest`, no field) throws a `TaxoraException`
|
|
1008
|
+
before any request. Switching an email on without any recipient answers `422` (`ValidationException`); a non-admin
|
|
1009
|
+
gets `403` with body code `admin_only` (or `read_only_role`) as `HttpException`. `notOnPeppol` only reports invoices
|
|
1010
|
+
held back after it was switched on.
|
|
1011
|
+
|
|
1012
|
+
### Billing usage summaries
|
|
1013
|
+
|
|
1014
|
+
Read-only usage of your billable modules (E-Reporting / E-Invoicing, Smart Enrichment), per feature and billing
|
|
1015
|
+
period.
|
|
1016
|
+
|
|
1017
|
+
```ts
|
|
1018
|
+
const page = await client.billing.listUsageSummaries(1, 25); // newest period first
|
|
1019
|
+
for (const summary of page) {
|
|
1020
|
+
console.log(summary.billingPeriod, summary.feature, summary.eventCount, summary.billableAmount, summary.state);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const september = await client.billing.getUsageSummaries('2026-09'); // with events; 404 for a period without usage
|
|
1024
|
+
console.log(september[0]?.events.length);
|
|
1025
|
+
|
|
1026
|
+
// E-invoicing allowance: documents included per month, used, still free, what is due beyond it (null without e-invoicing)
|
|
1027
|
+
const quota = await client.billing.getEInvoicingQuota();
|
|
1028
|
+
console.log(quota?.current.freeRemaining, 'of', quota?.current.included, 'documents free this month');
|
|
1029
|
+
```
|
|
1030
|
+
|
|
840
1031
|
---
|
|
841
1032
|
|
|
842
1033
|
## 📜 Certificates
|
|
@@ -922,6 +1113,13 @@ error body when there is one, and falls back to the status line
|
|
|
922
1113
|
(`Taxora API request failed (HTTP 504 Gateway Timeout).`) otherwise. The
|
|
923
1114
|
untouched body stays available via `err.getResponseBody()`.
|
|
924
1115
|
|
|
1116
|
+
**Compliance 403 codes.** Compliance 403s are `HttpException`s; the reason is the `code` in the JSON body
|
|
1117
|
+
(`err.getResponseBody()`): `service_not_active` (the company has not booked the service/country),
|
|
1118
|
+
`read_only_role` (a write request — create, update, delete, submit, import — by a user whose only role is
|
|
1119
|
+
read-only) or `admin_only` (body `{ message, code: 'admin_only' }`: a developer-role user tried to create an
|
|
1120
|
+
enrollment, `createCheckout()`, `confirmCheckout()`, `createBillingPortalSession()` or
|
|
1121
|
+
`updateNotificationSettings()` — only admins can add countries, change plans or change the notification emails).
|
|
1122
|
+
|
|
925
1123
|
### Automatic retries
|
|
926
1124
|
|
|
927
1125
|
The SDK retries what never produced an answer from the API itself: **3 attempts,
|
|
@@ -1038,6 +1236,43 @@ Next.js App Router: `const rawBody = await request.text();` (not `request.json()
|
|
|
1038
1236
|
`reason: 'invalid_body'`.
|
|
1039
1237
|
- **Idempotency:** dedupe on `event.id` (= `Taxora-Webhook-Id`), which stays the same across retries.
|
|
1040
1238
|
|
|
1239
|
+
### Events
|
|
1240
|
+
|
|
1241
|
+
| Event | `data.object` |
|
|
1242
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
1243
|
+
| `vat.validated` / `vat.state_changed` | `VatWebhookObject` (`id`, `vat_uid`, `state`, `company_name`, …) |
|
|
1244
|
+
| `compliance.transaction.state_changed` | `ComplianceTransactionWebhookObject` (`id`, `state`, `previous_state`, `provider_state`, `buyer_rejected`, …) |
|
|
1245
|
+
| `compliance.ledger.state_changed` | `ComplianceLedgerWebhookObject` (`id`, `state`, `ledger_type`, `ledger_date`) |
|
|
1246
|
+
| `enrichment.completed` | `EnrichmentCompletedWebhookObject` (`jobId`, `status`, `results`, …) |
|
|
1247
|
+
| `enrichment.updated` | `EnrichmentUpdatedWebhookObject` (`jobId`, `rowIndex`, `result`, …) |
|
|
1248
|
+
| `webhook.test` | `TestWebhookObject` (`message`) |
|
|
1249
|
+
|
|
1250
|
+
`compliance.transaction.state_changed` fires for **every real change** of a transaction: Taxora's own state
|
|
1251
|
+
(`submitted`, `error`, `not_required`) and the provider's delivery state (`accepted`, `refused`, `paid`, …). It is
|
|
1252
|
+
never sent twice for the same state and never for the transient `sending`. Besides `id`, `invoice_number`,
|
|
1253
|
+
`country` (lowercase), `state` and `provider_state` the object carries `internal_reference`, `previous_state`,
|
|
1254
|
+
`previous_provider_state`, `buyer_rejected`, `rejection_reason`, `submission_error` and `delivery_check` — these are
|
|
1255
|
+
optional in the type because older deliveries lack them.
|
|
1256
|
+
|
|
1257
|
+
```ts
|
|
1258
|
+
import { isTransactionBuyerRejected } from '@taxora/sdk';
|
|
1259
|
+
|
|
1260
|
+
if (event.event === 'compliance.transaction.state_changed') {
|
|
1261
|
+
const tx = event.data.object;
|
|
1262
|
+
if (tx.state === 'not_required') {
|
|
1263
|
+
// The buyer cannot receive e-invoices (tx.delivery_check e.g. 'not_reachable'):
|
|
1264
|
+
// nothing was sent or billed — send your PDF to the buyer by e-mail yourself.
|
|
1265
|
+
}
|
|
1266
|
+
if (isTransactionBuyerRejected(tx)) {
|
|
1267
|
+
// The buyer refused the e-invoice (tx.rejection_reason) — issue a credit note.
|
|
1268
|
+
}
|
|
1269
|
+
if (tx.state === 'error') console.warn(tx.submission_error);
|
|
1270
|
+
}
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
Transactions carry the same signal (`tx.buyerRejected`, `tx.rejectionReason`); find rejected invoices with
|
|
1274
|
+
`listTransactions({ providerState: 'refused' })`.
|
|
1275
|
+
|
|
1041
1276
|
---
|
|
1042
1277
|
|
|
1043
1278
|
## 🔌 VatState Helpers
|