@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 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
- └── company → CompanyEndpoint (company info)
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 filter optional
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