@getpeppr/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,463 @@
1
+ # @getpeppr/sdk
2
+
3
+ Send Peppol e-invoices in 5 lines of code.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@getpeppr/sdk.svg)](https://www.npmjs.com/package/@getpeppr/sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ ```typescript
10
+ import { Peppol } from "@getpeppr/sdk";
11
+
12
+ const peppol = new Peppol({ apiKey: "sk_live_..." });
13
+
14
+ const result = await peppol.invoices.send({
15
+ number: "INV-2026-001",
16
+ to: { name: "Acme Corp", peppolId: "0208:BE9876543210", country: "BE" },
17
+ lines: [
18
+ { description: "Consulting services", quantity: 10, unitPrice: 150, vatRate: 21 },
19
+ ],
20
+ });
21
+
22
+ console.log(`Invoice sent! Status: ${result.status}`);
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install @getpeppr/sdk
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - **Stripe-like developer experience** -- JSON in, Peppol out
34
+ - **Full Peppol BIS 3.0 compliance** -- UBL 2.1 XML generation handled for you
35
+ - **Client-side + server-side validation** -- catch errors before they reach the network
36
+ - **Create, send, list, acknowledge** invoices
37
+ - **Credit notes** with automatic UBL CreditNote generation
38
+ - **PDF and XML export** -- download invoices in any format
39
+ - **Peppol Directory lookup** -- verify participants before sending
40
+ - **Events API** with async pagination
41
+ - **Batch send** with concurrency control
42
+ - **Webhook signature verification** -- HMAC-SHA256 with replay protection
43
+ - **Built-in retry logic** with exponential backoff and rate limit handling
44
+ - **TypeScript-first** with full type safety
45
+ - **Zero dependencies**
46
+
47
+ ## Configuration
48
+
49
+ ```typescript
50
+ import { Peppol } from "@getpeppr/sdk";
51
+
52
+ const peppol = new Peppol({
53
+ apiKey: "sk_live_...", // Required. Starts with sk_sandbox_ or sk_live_
54
+ baseUrl: "https://...", // Custom gateway URL (default: https://api.getpeppr.dev/v1)
55
+ timeout: 30000, // Request timeout in ms (default: 30s)
56
+ retry: {
57
+ maxRetries: 3, // Max retry attempts (default: 3)
58
+ initialDelayMs: 500, // Initial retry delay (default: 500ms)
59
+ maxDelayMs: 30000, // Max retry delay (default: 30s)
60
+ },
61
+ });
62
+ ```
63
+
64
+ ## Sending Invoices
65
+
66
+ ### Basic invoice
67
+
68
+ ```typescript
69
+ const result = await peppol.invoices.send({
70
+ number: "INV-2026-001",
71
+ to: {
72
+ name: "Acme Corp",
73
+ peppolId: "0208:BE9876543210",
74
+ country: "BE",
75
+ },
76
+ lines: [
77
+ { description: "Consulting services", quantity: 10, unitPrice: 150, vatRate: 21 },
78
+ ],
79
+ });
80
+
81
+ console.log(result.id); // "inv-abc123"
82
+ console.log(result.status); // "new" | "sent" | ...
83
+ ```
84
+
85
+ ### Full invoice with all options
86
+
87
+ ```typescript
88
+ const result = await peppol.invoices.send({
89
+ // Required
90
+ number: "INV-2026-001",
91
+ to: {
92
+ name: "Acme Corp",
93
+ peppolId: "0208:BE9876543210",
94
+ vatNumber: "BE9876543210",
95
+ street: "123 Business Ave",
96
+ city: "Brussels",
97
+ postalCode: "1000",
98
+ country: "BE",
99
+ companyId: "0876543210",
100
+ contactName: "Jane Doe",
101
+ phone: "+32 2 123 4567",
102
+ email: "invoices@acme.be",
103
+ },
104
+ lines: [
105
+ {
106
+ description: "Consulting services",
107
+ quantity: 10,
108
+ unit: "hours", // Human-readable: "hours", "days", "kg", or UN/ECE: "HUR", "DAY"
109
+ unitPrice: 150,
110
+ vatRate: 21,
111
+ vatCategory: "S", // S=standard, Z=zero-rated, E=exempt, AE=reverse charge
112
+ itemId: "SRV-001",
113
+ allowances: [{ reason: "Loyalty discount", amount: 50 }],
114
+ charges: [{ reason: "Express delivery", amount: 25 }],
115
+ },
116
+ ],
117
+
118
+ // Optional
119
+ date: "2026-01-15", // Invoice date (default: today)
120
+ dueDate: "2026-02-15", // Due date
121
+ currency: "EUR", // ISO 4217 (default: EUR)
122
+ taxCurrency: "GBP", // Tax reporting currency if different
123
+ taxCurrencyRate: 0.86, // Exchange rate to tax currency
124
+ buyerReference: "PO-2026-042", // Buyer reference (BT-10)
125
+ orderReference: "ORD-789", // Purchase order number
126
+ note: "Thank you for your business",
127
+ paymentTerms: "Net 30 days",
128
+ paymentMeans: 30, // 30=credit transfer, 58=SEPA
129
+ paymentIban: "BE68539007547034",
130
+ paymentBic: "BBRUBEBB",
131
+ paymentReference: "+++090/9337/55493+++",
132
+
133
+ payeeParty: { // When payment recipient differs from seller
134
+ name: "Factoring Co",
135
+ peppolId: "0208:BE1111111111",
136
+ country: "BE",
137
+ },
138
+
139
+ invoicePeriod: { // Billing period (common for SaaS)
140
+ startDate: "2026-01-01",
141
+ endDate: "2026-01-31",
142
+ },
143
+
144
+ delivery: { // Delivery information
145
+ date: "2026-01-20",
146
+ locationId: "LOC-001",
147
+ address: { street: "456 Warehouse Rd", city: "Antwerp", postalCode: "2000", country: "BE" },
148
+ },
149
+
150
+ allowances: [{ reason: "Volume discount", amount: 100, vatRate: 21 }],
151
+ charges: [{ reason: "Handling fee", amount: 25, vatRate: 21 }],
152
+
153
+ attachments: [
154
+ {
155
+ id: "ATT-001",
156
+ description: "Timesheet",
157
+ filename: "timesheet.pdf",
158
+ mimeType: "application/pdf",
159
+ content: "base64-encoded-content...", // Or use `url` for external reference
160
+ },
161
+ ],
162
+ });
163
+ ```
164
+
165
+ ### Create draft + send later
166
+
167
+ ```typescript
168
+ const draft = await peppol.invoices.create({
169
+ number: "INV-2026-002",
170
+ to: { name: "Acme Corp", peppolId: "0208:BE9876543210", country: "BE" },
171
+ lines: [{ description: "Consulting", quantity: 5, unitPrice: 200, vatRate: 21 }],
172
+ });
173
+
174
+ console.log(draft.id); // "inv-xyz456"
175
+ console.log(draft.status); // "new"
176
+
177
+ // Send when ready
178
+ await peppol.invoices.sendById(draft.id);
179
+ ```
180
+
181
+ ### Credit notes
182
+
183
+ ```typescript
184
+ const result = await peppol.invoices.send({
185
+ number: "CN-2026-001",
186
+ isCreditNote: true,
187
+ invoiceReference: "INV-2026-001", // Required: original invoice number
188
+ to: { name: "Acme Corp", peppolId: "0208:BE9876543210", country: "BE" },
189
+ lines: [
190
+ { description: "Refund for consulting services", quantity: 2, unitPrice: 150, vatRate: 21 },
191
+ ],
192
+ });
193
+ ```
194
+
195
+ ## Listing Invoices
196
+
197
+ ### Single page
198
+
199
+ ```typescript
200
+ const page = await peppol.invoices.list({
201
+ limit: 25,
202
+ offset: 0,
203
+ type: "issued", // "issued" | "received"
204
+ includeLines: true, // Include line item details
205
+ });
206
+
207
+ console.log(page.data); // InvoiceSummary[]
208
+ console.log(page.meta); // { totalCount, offset, limit, hasMore }
209
+ ```
210
+
211
+ ### Auto-paginate all invoices
212
+
213
+ ```typescript
214
+ for await (const invoice of peppol.invoices.listAll({ type: "issued" })) {
215
+ console.log(invoice.id, invoice.number, invoice.status);
216
+ }
217
+ ```
218
+
219
+ ## Invoice Status
220
+
221
+ ### Get current status
222
+
223
+ ```typescript
224
+ const result = await peppol.invoices.getStatus("inv-abc123");
225
+ console.log(result.status); // "sent", "accepted", etc.
226
+ ```
227
+
228
+ ### Wait for a specific status
229
+
230
+ ```typescript
231
+ const result = await peppol.invoices.waitFor("inv-abc123", "accepted", {
232
+ timeout: 60000, // Max wait time in ms (default: 120s)
233
+ interval: 5000, // Polling interval in ms (default: 5s)
234
+ });
235
+ ```
236
+
237
+ You can also wait for multiple statuses:
238
+
239
+ ```typescript
240
+ const result = await peppol.invoices.waitFor("inv-abc123", ["accepted", "paid"]);
241
+ ```
242
+
243
+ Terminal failure statuses (`error`, `refused`, `invalid`) throw immediately without waiting for timeout.
244
+
245
+ ### Document status lifecycle
246
+
247
+ ```
248
+ new --> sent --> downloaded --> registered --> accepted --> paid --> closed
249
+ \-> refused
250
+ \-> error
251
+ ```
252
+
253
+ ## Export PDF / XML
254
+
255
+ ```typescript
256
+ import { writeFileSync } from "fs";
257
+
258
+ // Export as PDF
259
+ const pdf = await peppol.invoices.getAs("inv-abc123", "pdf");
260
+ writeFileSync("invoice.pdf", Buffer.from(pdf));
261
+
262
+ // Export as UBL XML
263
+ const xml = await peppol.invoices.getAs("inv-abc123", "xml.ubl.invoice.bis3");
264
+ writeFileSync("invoice.xml", Buffer.from(xml));
265
+
266
+ // Available formats: "pdf" | "xml.ubl.invoice.bis3" | "xml.facturae.3.2" | "original"
267
+ ```
268
+
269
+ ## Acknowledge Received Invoice
270
+
271
+ ```typescript
272
+ const result = await peppol.invoices.acknowledge("inv-abc123");
273
+ console.log(result.status); // "accepted"
274
+ ```
275
+
276
+ ## Batch Send
277
+
278
+ ```typescript
279
+ const invoices = [invoice1, invoice2, invoice3, /* ... */];
280
+
281
+ const result = await peppol.invoices.sendBatch(invoices, {
282
+ concurrency: 5, // Max parallel requests (default: 5)
283
+ stopOnError: false, // Continue on failure (default: false)
284
+ });
285
+
286
+ console.log(`${result.succeeded.length} sent, ${result.failed.length} failed`);
287
+
288
+ for (const failure of result.failed) {
289
+ console.error(`Invoice #${failure.index} (${failure.input.number}): ${failure.error.message}`);
290
+ }
291
+ ```
292
+
293
+ ## Peppol Directory
294
+
295
+ ```typescript
296
+ const entry = await peppol.directory.lookup("0208:BE0123456789");
297
+
298
+ console.log(entry.name); // "My Company NV"
299
+ console.log(entry.country); // "BE"
300
+ console.log(entry.capabilities); // ["invoice", "credit_note"]
301
+ ```
302
+
303
+ ## Validation
304
+
305
+ ### Client-side validation (instant, offline)
306
+
307
+ ```typescript
308
+ const result = peppol.validate({
309
+ number: "INV-001",
310
+ to: { name: "Acme", peppolId: "0208:BE9876543210", country: "BE" },
311
+ lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }],
312
+ });
313
+
314
+ if (!result.valid) {
315
+ for (const err of result.errors) {
316
+ console.log(`${err.field}: ${err.message}`);
317
+ if (err.suggestion) console.log(` Fix: ${err.suggestion}`);
318
+ }
319
+ }
320
+
321
+ for (const warn of result.warnings) {
322
+ console.log(`Warning: ${warn.field}: ${warn.message}`);
323
+ }
324
+ ```
325
+
326
+ ### Server-side validation (XSD + Schematron)
327
+
328
+ ```typescript
329
+ const result = await peppol.invoices.validateServer({
330
+ number: "INV-001",
331
+ to: { name: "Acme", peppolId: "0208:BE9876543210", country: "BE" },
332
+ lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }],
333
+ });
334
+
335
+ console.log(result.valid); // true/false
336
+ console.log(result.xsd.errors); // XSD schema errors
337
+ console.log(result.schematron.errors); // Peppol business rule errors
338
+ console.log(result.schematron.warnings); // Non-blocking warnings
339
+ ```
340
+
341
+ ## Events
342
+
343
+ ### List events
344
+
345
+ ```typescript
346
+ const result = await peppol.events.list({
347
+ limit: 10,
348
+ invoiceId: "inv-abc123", // Filter by invoice
349
+ dateFrom: "2026-01-01", // Filter by date range
350
+ dateTo: "2026-01-31",
351
+ });
352
+
353
+ for (const event of result.data) {
354
+ console.log(event.name, event.text, event.createdAt);
355
+ }
356
+ ```
357
+
358
+ ### Auto-paginate all events
359
+
360
+ ```typescript
361
+ for await (const event of peppol.events.listAll({ invoiceId: "inv-abc123" })) {
362
+ console.log(event.name, event.createdAt);
363
+ }
364
+ ```
365
+
366
+ ## Webhooks
367
+
368
+ Verify and parse incoming webhook payloads with HMAC-SHA256 signature verification and replay protection.
369
+
370
+ ```typescript
371
+ import { Peppol, webhooks } from "@getpeppr/sdk";
372
+
373
+ // Express example
374
+ app.post("/webhooks/peppol", async (req, res) => {
375
+ try {
376
+ const event = await webhooks.constructEvent(
377
+ req.body, // Raw body string (NOT parsed JSON)
378
+ req.headers["x-b2brouter-signature"], // Signature header
379
+ "whsec_your_webhook_secret", // Your webhook secret
380
+ );
381
+
382
+ switch (event.type) {
383
+ case "invoice.accepted":
384
+ console.log("Invoice accepted:", event.data);
385
+ break;
386
+ case "invoice.refused":
387
+ console.log("Invoice refused:", event.data);
388
+ break;
389
+ case "invoice.received":
390
+ console.log("New invoice received:", event.data);
391
+ break;
392
+ }
393
+
394
+ res.sendStatus(200);
395
+ } catch (err) {
396
+ console.error("Webhook verification failed:", err.message);
397
+ res.status(400).send("Webhook verification failed");
398
+ }
399
+ });
400
+ ```
401
+
402
+ Supported event types:
403
+
404
+ | Event | Description |
405
+ |-------|-------------|
406
+ | `invoice.sent` | Invoice sent to Peppol network |
407
+ | `invoice.accepted` | Recipient accepted the invoice |
408
+ | `invoice.refused` | Recipient refused the invoice |
409
+ | `invoice.error` | Delivery error |
410
+ | `invoice.registered` | Invoice registered by recipient |
411
+ | `invoice.paid` | Invoice marked as paid |
412
+ | `invoice.closed` | Invoice closed |
413
+ | `invoice.received` | New invoice received |
414
+ | `creditnote.sent` | Credit note sent |
415
+ | `creditnote.received` | Credit note received |
416
+
417
+ ## UBL XML Generation
418
+
419
+ Generate UBL 2.1 XML without sending (useful for debugging or manual submission):
420
+
421
+ ```typescript
422
+ const xml = peppol.toXml({
423
+ number: "INV-001",
424
+ to: { name: "Acme", peppolId: "0208:BE9876543210", country: "BE" },
425
+ lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }],
426
+ });
427
+
428
+ console.log(xml); // Full UBL 2.1 Invoice XML
429
+ ```
430
+
431
+ ## Error Handling
432
+
433
+ The SDK provides three error classes for precise error handling:
434
+
435
+ ```typescript
436
+ import { Peppol, PeppolError, PeppolValidationError, PeppolApiError } from "@getpeppr/sdk";
437
+
438
+ try {
439
+ await peppol.invoices.send(input);
440
+ } catch (err) {
441
+ if (err instanceof PeppolValidationError) {
442
+ // Client-side validation failed
443
+ console.log(err.validation.errors); // ValidationError[]
444
+ console.log(err.validation.warnings); // ValidationWarning[]
445
+ } else if (err instanceof PeppolApiError) {
446
+ // API returned an error
447
+ console.log(err.statusCode); // HTTP status code
448
+ console.log(err.responseBody); // Raw response body
449
+ console.log(err.retryAfterMs); // Retry-After delay (on 429)
450
+ } else if (err instanceof PeppolError) {
451
+ // General SDK error (invalid config, timeout, etc.)
452
+ console.log(err.message);
453
+ }
454
+ }
455
+ ```
456
+
457
+ ### Automatic retries
458
+
459
+ The SDK automatically retries on transient errors (429, 500, 502, 503, 504) with exponential backoff and jitter. Rate limit responses (429) respect the `Retry-After` header.
460
+
461
+ ## License
462
+
463
+ MIT