@taxora/sdk 1.8.0 → 1.9.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 +68 -0
- package/README.md +185 -4
- package/dist/index.cjs +863 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +478 -14
- package/dist/index.d.ts +478 -14
- package/dist/index.js +858 -179
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,74 @@
|
|
|
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.9.0] — 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`ComplianceCoverageCountry.support`** — `'yes' | 'cross_border_only' | 'coming_soon'` from
|
|
12
|
+
`GET /compliance/offerings`: whether a country is fully served, reached only for invoices across the border, or not
|
|
13
|
+
live yet (can still be requested). Defaults to `'yes'` for older API versions.
|
|
14
|
+
- **`eReporting.getNotificationSettings()` / `eReporting.updateNotificationSettings(input)`** —
|
|
15
|
+
`GET` / `PUT /compliance/notification-settings`: the company's opt-in compliance emails as
|
|
16
|
+
`ComplianceNotificationSettings` (`{ recipients, language, notOnPeppol, digest, digestSentAt }`). `notOnPeppol` =
|
|
17
|
+
hourly email listing invoices held back because the buyer is not on Peppol; `digest` = `'off' | 'daily' | 'weekly'`
|
|
18
|
+
summary. Updates send only the provided fields (`UpdateComplianceNotificationSettingsInput`, at least one; max 5
|
|
19
|
+
recipients, validated before the request); 422 when an email is switched on without recipients, 403 `admin_only` /
|
|
20
|
+
`read_only_role` for non-admins.
|
|
21
|
+
- **`eReporting.exportTransactionsCsv(filters)`** — `GET /compliance/transactions/export.csv`: all transactions
|
|
22
|
+
matching the `listTransactions()` filters (without pagination, type `ComplianceTransactionExportFilters`) as a
|
|
23
|
+
semicolon-separated CSV for bookkeeping, incl. internal reference, delivery state, buyer rejection and ECB EUR
|
|
24
|
+
columns (max 50,000 rows). Returned verbatim as a string.
|
|
25
|
+
- **`ComplianceTransaction.eurEquivalent`** (`{ subtotal, taxAmount, total, rate, rateDate, source: 'ECB' }`): the
|
|
26
|
+
amounts of a non-EUR invoice in EUR at the ECB reference rate of the invoice date; null for EUR invoices or when no
|
|
27
|
+
rate is known.
|
|
28
|
+
- **E-invoicing status signals:** `compliance.transaction.state_changed` now fires for every real change (Taxora's
|
|
29
|
+
own state and the provider's delivery state: accepted, refused, paid, …; never twice, never for `sending`).
|
|
30
|
+
`ComplianceTransactionWebhookObject` gained the optional `internal_reference`, `previous_state`,
|
|
31
|
+
`previous_provider_state`, `buyer_rejected`, `rejection_reason`, `submission_error` and `delivery_check`, plus the
|
|
32
|
+
helper **`isTransactionBuyerRejected(object)`**.
|
|
33
|
+
- **`ComplianceTransaction.buyerRejected` / `.rejectionReason`** and the **`providerState`** filter on
|
|
34
|
+
`listTransactions()` (e.g. `'refused'` = rejected by the buyer).
|
|
35
|
+
- **`eReporting.getTransactionPdf(id)`** downloads the invoice as a readable PDF (the attached seller PDF or the
|
|
36
|
+
provider's rendering of the e-invoice) as `{ content, contentType, filename }`; 409 while no PDF is available.
|
|
37
|
+
**`ComplianceTransaction.pdfUrl`** is set once a PDF is available.
|
|
38
|
+
- **Internal reference:** `internalReference` on `createTransaction()` and `updateTransaction()` (max 100 chars;
|
|
39
|
+
an empty string clears it on update) and `ComplianceTransaction.internalReference`. Your own reference (order
|
|
40
|
+
number, ERP id) for searching — never sent to the buyer or the tax authority. Also an optional CSV import column
|
|
41
|
+
`internal_reference`.
|
|
42
|
+
- **`listTransactions()` filters `search`** (contains-match over invoice number, internal reference and buyer name)
|
|
43
|
+
**and `internalReference`** (exact match), both max 100 chars.
|
|
44
|
+
- **`eReporting.getTransactionDocument(id)`** downloads the legal e-invoice of a sent transaction
|
|
45
|
+
(`{ content: Uint8Array, contentType, filename }`); 409 while it is not available yet.
|
|
46
|
+
- **Invoice PDF attachment:** `invoicePdf` / `invoicePdfFilename` on `createTransaction()` and `updateTransaction()`
|
|
47
|
+
(e-invoicing only). Bytes (`Buffer` / `Uint8Array` / `ArrayBuffer`) are checked (PDF, max 10 MB) and base64-encoded;
|
|
48
|
+
strings are sent as-is (already base64).
|
|
49
|
+
- **`ComplianceTransaction.documentUrl`, `.deliveryCheck` and `.invoicePdf`** (`{ filename, size, attached }`).
|
|
50
|
+
- **`ComplianceTransactionState.NOT_REQUIRED`** (`not_required`): the buyer cannot receive e-invoices, so nothing was
|
|
51
|
+
sent or billed.
|
|
52
|
+
- **`eReporting.exportRevenueTaxRatesCsv(filters)`**: net and VAT per month and tax rate as CSV (BOM preserved).
|
|
53
|
+
- **Self-service plans:** `eReporting.getOfferings()` (`ComplianceOfferings`), `createCheckout(country, plan)`,
|
|
54
|
+
`confirmCheckout(sessionId)` and `createBillingPortalSession()`.
|
|
55
|
+
- **`client.billing`** (`BillingEndpoint`): `listUsageSummaries(page, perPage)` (`UsageSummaryPage`) and
|
|
56
|
+
`getUsageSummaries(period)` (`UsageSummary[]` with events).
|
|
57
|
+
- **Per-country filters:** `complianceEnrollmentId` on `getRevenueStatistics()` and `exportRevenueTaxRatesCsv()`;
|
|
58
|
+
`country` as 4th argument of `listTaxReports(page, perPage, state, country)`.
|
|
59
|
+
- README: compliance write requests by a read-only user return 403 with code `read_only_role` (HttpException).
|
|
60
|
+
- README: compliance webhook events (`compliance.transaction.state_changed`, `compliance.ledger.state_changed`) listed
|
|
61
|
+
with their payloads.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **`ComplianceOfferingPlan.requires`** is now typed `{ country: string; service: string } | null` and parsed from the
|
|
66
|
+
object the API sends (e.g. `{ country: 'FR', service: 'e_reporting' }`). It was typed `unknown[] | null` and always
|
|
67
|
+
came back `null`. Public type change.
|
|
68
|
+
- `createTransaction()` / `updateTransaction()` reject `invoicePdfFilename` without `invoicePdf`
|
|
69
|
+
(`invoicePdfFilename requires invoicePdf.`), like the PHP SDK.
|
|
70
|
+
- README: `admin_only` (403) listed with the compliance error codes; the webhook example no longer mentions a
|
|
71
|
+
`not_required` state event (it is never sent).
|
|
72
|
+
|
|
5
73
|
## [1.8.0] — 2026-10-25
|
|
6
74
|
|
|
7
75
|
### 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
|
|
@@ -837,6 +881,99 @@ console.log(creditNote.isCreditNote); // true
|
|
|
837
881
|
const noDocs = await client.eReporting.listTransactions({ country: 'NO' });
|
|
838
882
|
```
|
|
839
883
|
|
|
884
|
+
**6. Attach your own invoice PDF** (e-invoicing only, max 10 MB) — pass the bytes (`Buffer` / `Uint8Array` /
|
|
885
|
+
`ArrayBuffer`); the SDK base64-encodes them. A string is sent as-is and must already be base64. The structured
|
|
886
|
+
e-invoice stays the legal document; whether the buyer's system shows the PDF depends on that system.
|
|
887
|
+
|
|
888
|
+
```ts
|
|
889
|
+
import { readFileSync } from 'fs';
|
|
890
|
+
|
|
891
|
+
const withPdf = await client.eReporting.createTransaction({
|
|
892
|
+
...invoiceInput,
|
|
893
|
+
invoicePdf: readFileSync('NO-2026-0002.pdf'),
|
|
894
|
+
invoicePdfFilename: 'NO-2026-0002.pdf', // optional, defaults to the invoice number
|
|
895
|
+
});
|
|
896
|
+
console.log(withPdf.invoicePdf); // { filename, size, attached } — attached once uploaded with the e-invoice
|
|
897
|
+
|
|
898
|
+
await client.eReporting.updateTransaction(withPdf.id, { invoicePdf: readFileSync('fixed.pdf') }); // replaces it
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
**7. Buyer not on Peppol / download the legal document**
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
const tx = await client.eReporting.getTransaction(invoice.id);
|
|
905
|
+
|
|
906
|
+
if (tx.state === 'not_required') {
|
|
907
|
+
// The buyer cannot receive e-invoices: nothing was sent or billed — send your regular invoice.
|
|
908
|
+
console.log(tx.deliveryCheck); // Peppol directory answer, e.g. { status: 'not_reachable', … }
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (tx.documentUrl !== null) {
|
|
912
|
+
const doc = await client.eReporting.getTransactionDocument(tx.id); // 409 while not available yet
|
|
913
|
+
writeFileSync(doc.filename ?? `${tx.invoiceNumber}.xml`, doc.content); // doc.contentType e.g. application/xml
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// The invoice as a readable PDF (your attached PDF, or the provider's rendering of the e-invoice)
|
|
917
|
+
if (tx.pdfUrl !== null) {
|
|
918
|
+
const pdf = await client.eReporting.getTransactionPdf(tx.id); // 409 while no PDF is available
|
|
919
|
+
writeFileSync(pdf.filename ?? `${tx.invoiceNumber}.pdf`, pdf.content); // pdf.contentType: application/pdf
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### Self-service plans (Stripe)
|
|
924
|
+
|
|
925
|
+
Book compliance plans without contacting sales. These routes work for companies **without** compliance access.
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
const { offerings, coverage, access, accessByCountry } = await client.eReporting.getOfferings();
|
|
929
|
+
const plan = offerings.find((o) => o.country === 'NO')?.plans.find((p) => p.checkoutAvailable);
|
|
930
|
+
|
|
931
|
+
const session = await client.eReporting.createCheckout('NO', plan!.key); // 422 if contact-only / already active
|
|
932
|
+
// open session.url in the browser; Stripe returns the user to the Taxora portal
|
|
933
|
+
const state = await client.eReporting.confirmCheckout(session.id); // idempotent → { active, country, service, plan, status }
|
|
934
|
+
|
|
935
|
+
const portalUrl = await client.eReporting.createBillingPortalSession(); // invoices, payment method, cancel; 404 without a Stripe plan
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### Notification emails
|
|
939
|
+
|
|
940
|
+
Opt-in e-reporting / e-invoicing emails for your company — readable by every member, changeable by company **admins**
|
|
941
|
+
only.
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
const settings = await client.eReporting.getNotificationSettings();
|
|
945
|
+
// { recipients: ['accounting@example.com'], language: 'de' | 'en', notOnPeppol: boolean,
|
|
946
|
+
// digest: 'off' | 'daily' | 'weekly', digestSentAt: string | null }
|
|
947
|
+
|
|
948
|
+
// Only the provided fields change (at least one is required); recipients replaces the whole list (max 5):
|
|
949
|
+
await client.eReporting.updateNotificationSettings({
|
|
950
|
+
recipients: ['accounting@example.com', 'cfo@example.com'],
|
|
951
|
+
language: 'en',
|
|
952
|
+
notOnPeppol: true, // hourly email listing invoices held back because the buyer is not on Peppol
|
|
953
|
+
digest: 'weekly', // 'daily' = 08:00 for the day before, 'weekly' = Mondays for the last 7 days
|
|
954
|
+
});
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
Invalid input (more than 5 or invalid addresses, unknown `language` / `digest`, no field) throws a `TaxoraException`
|
|
958
|
+
before any request. Switching an email on without any recipient answers `422` (`ValidationException`); a non-admin
|
|
959
|
+
gets `403` with body code `admin_only` (or `read_only_role`) as `HttpException`. `notOnPeppol` only reports invoices
|
|
960
|
+
held back after it was switched on.
|
|
961
|
+
|
|
962
|
+
### Billing usage summaries
|
|
963
|
+
|
|
964
|
+
Read-only usage of your billable modules (E-Reporting / E-Invoicing, Smart Enrichment), per feature and billing
|
|
965
|
+
period.
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
const page = await client.billing.listUsageSummaries(1, 25); // newest period first
|
|
969
|
+
for (const summary of page) {
|
|
970
|
+
console.log(summary.billingPeriod, summary.feature, summary.eventCount, summary.billableAmount, summary.state);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const september = await client.billing.getUsageSummaries('2026-09'); // with events; 404 for a period without usage
|
|
974
|
+
console.log(september[0]?.events.length);
|
|
975
|
+
```
|
|
976
|
+
|
|
840
977
|
---
|
|
841
978
|
|
|
842
979
|
## 📜 Certificates
|
|
@@ -922,6 +1059,13 @@ error body when there is one, and falls back to the status line
|
|
|
922
1059
|
(`Taxora API request failed (HTTP 504 Gateway Timeout).`) otherwise. The
|
|
923
1060
|
untouched body stays available via `err.getResponseBody()`.
|
|
924
1061
|
|
|
1062
|
+
**Compliance 403 codes.** Compliance 403s are `HttpException`s; the reason is the `code` in the JSON body
|
|
1063
|
+
(`err.getResponseBody()`): `service_not_active` (the company has not booked the service/country),
|
|
1064
|
+
`read_only_role` (a write request — create, update, delete, submit, import — by a user whose only role is
|
|
1065
|
+
read-only) or `admin_only` (body `{ message, code: 'admin_only' }`: a developer-role user tried to create an
|
|
1066
|
+
enrollment, `createCheckout()`, `confirmCheckout()`, `createBillingPortalSession()` or
|
|
1067
|
+
`updateNotificationSettings()` — only admins can add countries, change plans or change the notification emails).
|
|
1068
|
+
|
|
925
1069
|
### Automatic retries
|
|
926
1070
|
|
|
927
1071
|
The SDK retries what never produced an answer from the API itself: **3 attempts,
|
|
@@ -1038,6 +1182,43 @@ Next.js App Router: `const rawBody = await request.text();` (not `request.json()
|
|
|
1038
1182
|
`reason: 'invalid_body'`.
|
|
1039
1183
|
- **Idempotency:** dedupe on `event.id` (= `Taxora-Webhook-Id`), which stays the same across retries.
|
|
1040
1184
|
|
|
1185
|
+
### Events
|
|
1186
|
+
|
|
1187
|
+
| Event | `data.object` |
|
|
1188
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
1189
|
+
| `vat.validated` / `vat.state_changed` | `VatWebhookObject` (`id`, `vat_uid`, `state`, `company_name`, …) |
|
|
1190
|
+
| `compliance.transaction.state_changed` | `ComplianceTransactionWebhookObject` (`id`, `state`, `previous_state`, `provider_state`, `buyer_rejected`, …) |
|
|
1191
|
+
| `compliance.ledger.state_changed` | `ComplianceLedgerWebhookObject` (`id`, `state`, `ledger_type`, `ledger_date`) |
|
|
1192
|
+
| `enrichment.completed` | `EnrichmentCompletedWebhookObject` (`jobId`, `status`, `results`, …) |
|
|
1193
|
+
| `enrichment.updated` | `EnrichmentUpdatedWebhookObject` (`jobId`, `rowIndex`, `result`, …) |
|
|
1194
|
+
| `webhook.test` | `TestWebhookObject` (`message`) |
|
|
1195
|
+
|
|
1196
|
+
`compliance.transaction.state_changed` fires for **every real change** of a transaction: Taxora's own state
|
|
1197
|
+
(`submitted`, `error`, `not_required`) and the provider's delivery state (`accepted`, `refused`, `paid`, …). It is
|
|
1198
|
+
never sent twice for the same state and never for the transient `sending`. Besides `id`, `invoice_number`,
|
|
1199
|
+
`country` (lowercase), `state` and `provider_state` the object carries `internal_reference`, `previous_state`,
|
|
1200
|
+
`previous_provider_state`, `buyer_rejected`, `rejection_reason`, `submission_error` and `delivery_check` — these are
|
|
1201
|
+
optional in the type because older deliveries lack them.
|
|
1202
|
+
|
|
1203
|
+
```ts
|
|
1204
|
+
import { isTransactionBuyerRejected } from '@taxora/sdk';
|
|
1205
|
+
|
|
1206
|
+
if (event.event === 'compliance.transaction.state_changed') {
|
|
1207
|
+
const tx = event.data.object;
|
|
1208
|
+
if (tx.state === 'not_required') {
|
|
1209
|
+
// The buyer cannot receive e-invoices (tx.delivery_check e.g. 'not_reachable'):
|
|
1210
|
+
// nothing was sent or billed — send your PDF to the buyer by e-mail yourself.
|
|
1211
|
+
}
|
|
1212
|
+
if (isTransactionBuyerRejected(tx)) {
|
|
1213
|
+
// The buyer refused the e-invoice (tx.rejection_reason) — issue a credit note.
|
|
1214
|
+
}
|
|
1215
|
+
if (tx.state === 'error') console.warn(tx.submission_error);
|
|
1216
|
+
}
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
Transactions carry the same signal (`tx.buyerRejected`, `tx.rejectionReason`); find rejected invoices with
|
|
1220
|
+
`listTransactions({ providerState: 'refused' })`.
|
|
1221
|
+
|
|
1041
1222
|
---
|
|
1042
1223
|
|
|
1043
1224
|
## 🔌 VatState Helpers
|