@taxora/sdk 1.2.0 → 1.4.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 +37 -0
- package/README.md +198 -0
- package/dist/index.cjs +813 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +543 -21
- package/dist/index.d.ts +543 -21
- package/dist/index.js +788 -88
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@taxora/sdk` are documented here. Versions follow [SemVer](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [1.4.0] — 2026-10-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Smart Enrichment: rejected VAT candidates.** `SmartEnrichmentResource.rejectedCandidates` lists VAT numbers
|
|
10
|
+
a `not_found` lookup DID find but the official check threw out, each with a `reason`
|
|
11
|
+
(`SmartEnrichmentRejectionReason`: `vat_invalid`, `name_mismatch`, `unbound`, `unverified`, `rejected`;
|
|
12
|
+
unknown values map to `unknown`). Lets you tell "found one, it is invalid" apart from "found nothing".
|
|
13
|
+
Plus `hasRejectedCandidates()`, `toSmartEnrichmentRejectionReason()` and
|
|
14
|
+
`describeSmartEnrichmentRejectionReason()`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Bundled OpenAPI specs (`resources/`) refreshed from the current API.
|
|
19
|
+
|
|
20
|
+
## [1.3.0] — 2026-10-13
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Webhook signature verification** for Taxora's opt-in HMAC-signed webhooks (`Taxora-Signature: t=…,v1=…`):
|
|
25
|
+
- `verifyWebhookSignature(rawBody, header, secret, { tolerance })` — constant-time comparison, timestamp
|
|
26
|
+
tolerance against replays (default 300 s), accepts several `v1` entries (rotation) and several secrets.
|
|
27
|
+
- `constructWebhookEvent(rawBody, header, secret, options)` — verifies, then returns a typed
|
|
28
|
+
`TaxoraWebhookEvent` (discriminated union on `event`).
|
|
29
|
+
- `generateWebhookSignatureHeader()` for testing your own handlers, plus `parseWebhookSignatureHeader()` and
|
|
30
|
+
`computeWebhookSignature()`.
|
|
31
|
+
- `WebhookSignatureVerificationError` with a machine-readable `reason`.
|
|
32
|
+
- Event types: `vat.validated`, `vat.state_changed`, `compliance.ledger.state_changed`,
|
|
33
|
+
`compliance.transaction.state_changed`, `enrichment.completed`, `enrichment.updated`, `webhook.test`.
|
|
34
|
+
|
|
35
|
+
## [1.2.0]
|
|
36
|
+
|
|
37
|
+
- Per-country compliance services incl. Norway e-invoicing (EHF / Peppol). See the GitHub release notes.
|
package/README.md
CHANGED
|
@@ -151,6 +151,12 @@ Each endpoint handles:
|
|
|
151
151
|
| `Token` | Access token with type and expiry; 15-second buffer for proactive refresh |
|
|
152
152
|
| `VatCertificateExport` | Bulk export job reference (`exportId` + optional message) |
|
|
153
153
|
| `ProviderDocument` | Attached provider document metadata (type, date, MIME, hash, nested line item) |
|
|
154
|
+
| `ComplianceEnrollment` | Per-country compliance enrollment: country, regime, `service`, provisioning status |
|
|
155
|
+
| `ComplianceTransaction` | Recorded transaction / e-invoice: amounts, state, buyer fields, `documentType`, `providerState` |
|
|
156
|
+
| `ComplianceTaxReport` | DGFiP tax report lifecycle of a transaction (France) |
|
|
157
|
+
| `SireneLookupResult` | Company data from the French SIRENE registry |
|
|
158
|
+
| `RegistryCompany` | Company from a national register (Norway: Brønnøysund) — org number, VAT / register flags |
|
|
159
|
+
| `PeppolLookupResult` | Peppol directory lookup: `status` (reachable / not_reachable / pending), document types |
|
|
154
160
|
|
|
155
161
|
All DTOs expose `toArray()` for serialization and a static `fromArray()` factory.
|
|
156
162
|
|
|
@@ -357,6 +363,13 @@ if (quality && quality.warnings.length > 0) {
|
|
|
357
363
|
console.log(quality.warnings.join(', '), '→', quality.derivedPlace);
|
|
358
364
|
}
|
|
359
365
|
|
|
366
|
+
// 'not_found' doesn't always mean nothing turned up. When the research found a VAT number but
|
|
367
|
+
// the official check threw it out (e.g. the tax authority reports it as invalid), the number
|
|
368
|
+
// and the reason are in `rejectedCandidates` — no need to re-check it yourself.
|
|
369
|
+
for (const candidate of result?.rejectedCandidates ?? []) {
|
|
370
|
+
console.log(candidate.vatNumber, candidate.reason); // e.g. 'vat_invalid'
|
|
371
|
+
}
|
|
372
|
+
|
|
360
373
|
// Bulk lookup (always async → webhook + polling)
|
|
361
374
|
// The batch-level mode applies to every row; a row carrying its own `mode` overrides it,
|
|
362
375
|
// so you can pay for 'complex' on just the rows that need it.
|
|
@@ -443,6 +456,12 @@ submit transactions (individually or via CSV import), and track the resulting DG
|
|
|
443
456
|
All `compliance` routes require an active E-Reporting subscription / feature grant on your
|
|
444
457
|
account — except `requestEReportingAccess()`, which is how you ask for one.
|
|
445
458
|
|
|
459
|
+
**Per-country services:** each enrollment books one service for one country — `e_reporting`
|
|
460
|
+
(France, regime `dgfip_flux10`) or `e_invoicing` (Norway EHF / Peppol BIS 3.0, regime
|
|
461
|
+
`peppol_bis3`, see [below](#-e-invoicing-norway-ehf--peppol)). Every country is unlocked and
|
|
462
|
+
billed separately (one-off setup fee + per document). Enrolling for a service your company does not
|
|
463
|
+
have yields a 403 `HttpException` whose body carries `code: "service_not_active"`.
|
|
464
|
+
|
|
446
465
|
### Request feature activation
|
|
447
466
|
|
|
448
467
|
Not enabled yet? This route is deliberately **not** gated — it requests activation of the
|
|
@@ -455,6 +474,8 @@ await client.eReporting.requestEReportingAccess({
|
|
|
455
474
|
phone: '+43 660 1234567',
|
|
456
475
|
message: 'Please activate e-reporting for our account.',
|
|
457
476
|
language: 'de', // locale of the confirmation mail
|
|
477
|
+
service: 'e_invoicing', // optional: 'e_reporting' (default) | 'e_invoicing'
|
|
478
|
+
countries: ['NO'], // optional: the countries you want the service for
|
|
458
479
|
});
|
|
459
480
|
```
|
|
460
481
|
|
|
@@ -541,6 +562,8 @@ const transactions = await client.eReporting.listTransactions({
|
|
|
541
562
|
dateTo: '2026-07-31',
|
|
542
563
|
state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error'
|
|
543
564
|
transactionType: 'b2c_outbound',
|
|
565
|
+
complianceEnrollmentId: enrollment.id, // optional: one enrollment only
|
|
566
|
+
country: 'FR', // optional: one country only
|
|
544
567
|
page: 1,
|
|
545
568
|
perPage: 25,
|
|
546
569
|
});
|
|
@@ -624,6 +647,119 @@ for (const row of stats.byCountry) {
|
|
|
624
647
|
}
|
|
625
648
|
```
|
|
626
649
|
|
|
650
|
+
### 🇳🇴 E-Invoicing Norway (EHF / Peppol)
|
|
651
|
+
|
|
652
|
+
Send EHF e-invoices and credit notes (Peppol BIS Billing 3.0) to Norwegian buyers. The
|
|
653
|
+
`e_invoicing` service for Norway is billed separately from French e-reporting (one-off setup
|
|
654
|
+
fee + per document) — ask for it via `requestEReportingAccess({ service: 'e_invoicing', countries: ['NO'] })`.
|
|
655
|
+
|
|
656
|
+
**1. Registry lookup** — resolve an organisation number, `NO…MVA` VAT number or company name in
|
|
657
|
+
the Brønnøysund register (rate-limited to 30 requests/minute; 404 when nothing is found):
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
import { NorwegianOrgNumber } from '@taxora/sdk';
|
|
661
|
+
|
|
662
|
+
const [company] = await client.eReporting.registryLookup('NO', '923609016');
|
|
663
|
+
console.log(company.companyName, company.vatNumber, company.vatRegistered, company.enterpriseRegister);
|
|
664
|
+
|
|
665
|
+
// Client-side helper (mod-11 check digit, accepts spaces / NO prefix / MVA suffix)
|
|
666
|
+
NorwegianOrgNumber.isValid('NO 923 609 016 MVA'); // true
|
|
667
|
+
NorwegianOrgNumber.normalize('923609016MVA'); // '923609016'
|
|
668
|
+
NorwegianOrgNumber.toVatNumber('923609016'); // 'NO923609016MVA'
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**2. Enrollment** — registers your company as a Peppol participant. The organisation number is
|
|
672
|
+
validated and normalized before sending:
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
const noEnrollment = await client.eReporting.createNorwayEnrollment({
|
|
676
|
+
orgNumber: company.orgNumber,
|
|
677
|
+
companyName: company.companyName,
|
|
678
|
+
address: company.address!,
|
|
679
|
+
city: company.city!,
|
|
680
|
+
postalcode: company.postalcode!,
|
|
681
|
+
email: 'faktura@acme.no',
|
|
682
|
+
vatRegistered: company.vatRegistered, // optional, default true → "MVA" on invoices
|
|
683
|
+
enterpriseRegister: company.enterpriseRegister, // optional, default false → "Foretaksregisteret"
|
|
684
|
+
reception: false, // optional: also receive e-invoices over Peppol
|
|
685
|
+
});
|
|
686
|
+
// Equivalent: client.eReporting.createEnrollment({ country: 'NO', ... })
|
|
687
|
+
|
|
688
|
+
console.log(noEnrollment.service, noEnrollment.regime); // 'e_invoicing' 'peppol_bis3'
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**3. Peppol lookup** — check that the buyer can receive EHF invoices (free of charge). The
|
|
692
|
+
directory resolves asynchronously: `pending` means ask again in a few seconds.
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
let reach = await client.eReporting.peppolLookup('NO', '974760673'); // scheme defaults to 0192
|
|
696
|
+
if (reach.isPending) {
|
|
697
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
698
|
+
reach = await client.eReporting.peppolLookup('NO', '974760673');
|
|
699
|
+
}
|
|
700
|
+
console.log(reach.status, reach.reachable, reach.documentTypes); // 'reachable' true [...]
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
**4. Send an invoice** — Norway requires `transactionType: 'b2b_domestic_outbound'`, the buyer's
|
|
704
|
+
organisation number (`counterpartyRegisterId`) and `counterpartyName` (enforced server-side, 422):
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
const invoice = await client.eReporting.createTransaction({
|
|
708
|
+
complianceEnrollmentId: noEnrollment.id,
|
|
709
|
+
transactionType: 'b2b_domestic_outbound',
|
|
710
|
+
invoiceNumber: 'NO-2026-0001',
|
|
711
|
+
invoiceDate: '2026-09-25',
|
|
712
|
+
dueDate: '2026-10-09',
|
|
713
|
+
currency: 'NOK',
|
|
714
|
+
subtotal: 2000,
|
|
715
|
+
taxAmount: 500,
|
|
716
|
+
total: 2500,
|
|
717
|
+
counterpartyName: 'EXAMPLE BUYER AS',
|
|
718
|
+
counterpartyCountry: 'NO',
|
|
719
|
+
counterpartyRegisterId: '974760673',
|
|
720
|
+
counterpartyAddress: 'Storgata 1',
|
|
721
|
+
counterpartyCity: 'Oslo',
|
|
722
|
+
counterpartyPostalcode: '0155',
|
|
723
|
+
counterpartyEmail: 'ap@buyer.example', // optional
|
|
724
|
+
buyerReference: 'PO-4711', // optional, defaults to the buyer's org number
|
|
725
|
+
invoiceLines: [
|
|
726
|
+
{ description: 'Consulting', quantity: 2, price: 1000, taxes: [{ name: 'MVA', percent: 25, category: 'S' }] },
|
|
727
|
+
],
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// `state` tracks submission; `providerState` the Peppol delivery (e.g. sent, accepted, refused, paid)
|
|
731
|
+
const current = await client.eReporting.getTransaction(invoice.id);
|
|
732
|
+
console.log(current.state, current.providerState, current.documentType); // … 'invoice'
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
**5. Send a credit note** — set `isCreditNote` and reference the corrected invoice:
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
const creditNote = await client.eReporting.createTransaction({
|
|
739
|
+
complianceEnrollmentId: noEnrollment.id,
|
|
740
|
+
transactionType: 'b2b_domestic_outbound',
|
|
741
|
+
invoiceNumber: 'NO-2026-0001-CN',
|
|
742
|
+
invoiceDate: '2026-09-30',
|
|
743
|
+
currency: 'NOK',
|
|
744
|
+
subtotal: 1000,
|
|
745
|
+
taxAmount: 250,
|
|
746
|
+
total: 1250,
|
|
747
|
+
counterpartyName: 'EXAMPLE BUYER AS',
|
|
748
|
+
counterpartyRegisterId: '974760673',
|
|
749
|
+
isCreditNote: true,
|
|
750
|
+
amendedNumber: 'NO-2026-0001', // required for credit notes in Norway
|
|
751
|
+
amendedDate: '2026-09-25',
|
|
752
|
+
extraInfo: 'Credit for one returned consulting day',
|
|
753
|
+
invoiceLines: [
|
|
754
|
+
{ description: 'Consulting', quantity: 1, price: 1000, taxes: [{ name: 'MVA', percent: 25, category: 'S' }] },
|
|
755
|
+
],
|
|
756
|
+
});
|
|
757
|
+
console.log(creditNote.isCreditNote); // true
|
|
758
|
+
|
|
759
|
+
// All Norwegian documents
|
|
760
|
+
const noDocs = await client.eReporting.listTransactions({ country: 'NO' });
|
|
761
|
+
```
|
|
762
|
+
|
|
627
763
|
---
|
|
628
764
|
|
|
629
765
|
## 📜 Certificates
|
|
@@ -765,6 +901,68 @@ const noRetries = TaxoraClientFactory.create({ apiKey: '…', retryPolicy: Retry
|
|
|
765
901
|
|
|
766
902
|
---
|
|
767
903
|
|
|
904
|
+
## 🔐 Webhook Signatures
|
|
905
|
+
|
|
906
|
+
Taxora can sign every webhook request with HMAC-SHA256 (opt-in per endpoint: dashboard → **Developer → Webhooks**,
|
|
907
|
+
or `POST /v1/webhooks/{id}/signing/enable`). Signed requests carry:
|
|
908
|
+
|
|
909
|
+
```
|
|
910
|
+
Taxora-Signature: t=1716115200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
|
|
911
|
+
Taxora-Webhook-Id: 550e8400-e29b-41d4-a716-446655440000
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
`constructWebhookEvent` verifies the signature (constant-time, 5-minute replay window) and returns a typed event:
|
|
915
|
+
|
|
916
|
+
```ts
|
|
917
|
+
import express from 'express';
|
|
918
|
+
import { constructWebhookEvent, WebhookSignatureVerificationError } from '@taxora/sdk';
|
|
919
|
+
|
|
920
|
+
const app = express();
|
|
921
|
+
|
|
922
|
+
// express.raw — NOT express.json: the signature covers the exact bytes Taxora sent.
|
|
923
|
+
app.post('/taxora-webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
924
|
+
let event;
|
|
925
|
+
try {
|
|
926
|
+
event = constructWebhookEvent(req.body, req.header('Taxora-Signature'), process.env.TAXORA_WEBHOOK_SECRET!, {
|
|
927
|
+
tolerance: 300, // seconds, default
|
|
928
|
+
});
|
|
929
|
+
} catch (err) {
|
|
930
|
+
if (err instanceof WebhookSignatureVerificationError) {
|
|
931
|
+
return res.status(400).send(err.message); // err.reason: 'no_matching_signature' | 'timestamp_outside_tolerance' | …
|
|
932
|
+
}
|
|
933
|
+
throw err;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
switch (event.event) {
|
|
937
|
+
case 'vat.validated':
|
|
938
|
+
console.log(event.data.object.vat_uid, event.data.object.state); // typed
|
|
939
|
+
break;
|
|
940
|
+
case 'enrichment.completed':
|
|
941
|
+
console.log(event.data.object.foundCount);
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
res.sendStatus(200);
|
|
946
|
+
});
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Next.js App Router: `const rawBody = await request.text();` (not `request.json()`).
|
|
950
|
+
|
|
951
|
+
| Helper | Purpose |
|
|
952
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
953
|
+
| `verifyWebhookSignature(rawBody, header, secret, { tolerance })` | Returns `true` or throws `WebhookSignatureVerificationError` |
|
|
954
|
+
| `constructWebhookEvent(rawBody, header, secret, { tolerance })` | Verifies, then parses into `TaxoraWebhookEvent` |
|
|
955
|
+
| `generateWebhookSignatureHeader(rawBody, secret, timestamp?)` | Builds a valid header — for testing your own handler |
|
|
956
|
+
| `parseWebhookSignatureHeader(header)` / `computeWebhookSignature(...)` | Low-level building blocks |
|
|
957
|
+
|
|
958
|
+
- **Rotation:** while Taxora rotates your secret, requests carry one `v1=` per live secret — any match is accepted.
|
|
959
|
+
`secret` may also be an array, so you can accept old and new secret on your side during a deploy.
|
|
960
|
+
- **Raw body:** pass a `string`, `Buffer` or `Uint8Array`. Passing an already parsed object throws
|
|
961
|
+
`reason: 'invalid_body'`.
|
|
962
|
+
- **Idempotency:** dedupe on `event.id` (= `Taxora-Webhook-Id`), which stays the same across retries.
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
768
966
|
## 🔌 VatState Helpers
|
|
769
967
|
|
|
770
968
|
```ts
|