@billium/node 1.0.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 ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@billium/node` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [1.0.0]
6
+
7
+ Initial public release. The API surface is covered by the SemVer guarantee from this point on — breaking changes will only ship in major releases.
8
+
9
+ ### Added
10
+
11
+ - **Invoices client.** `billium.invoices.create / get / list / cancel`. The `Invoice` response shape mirrors the backend exactly: customer information is nested under `invoice.customer` (as `InvoiceCustomer | null`), and `rawAmount` / `endAmount` are returned as **strings** (Prisma `Decimal(15,6)` serialized) rather than numbers — use a decimal library like `decimal.js` for arithmetic.
12
+ - **Webhook signature verification.** `billium.webhooks.verify(rawBody, signatureHeader)` performs HMAC-SHA256 with timing-safe comparison and a configurable tolerance window. Drop-in for Express, Fastify, Hono, Next.js Route Handlers — anywhere you can get the raw request body.
13
+ - **Webhook management.** `billium.webhooks.create / list / update / delete / ping` for managing webhook endpoints from server code. Requires a secret key.
14
+ - **Idempotency-Key support** on `invoices.create()` via `{ idempotencyKey }` option. The server deduplicates retries within a 24 h window so a network blip can't create duplicate invoices. Generate a UUID v4 per logical operation.
15
+ - **Automatic retries** for transient failures (`5xx`, `429`, network errors) with exponential backoff and full jitter. Honors `Retry-After` headers when present. Configurable via `maxRetries`, `baseDelayMs`, `maxDelayMs` on the `Billium` constructor (defaults: 2 retries, 500 ms base, 30 s cap). `POST` is only retried when an `Idempotency-Key` header is set — without one, the SDK refuses to retry POSTs to avoid creating duplicates.
16
+ - **Public-key safety guard.** Billium issues two API key types: `pk_*` (public, scope-limited) and `sk_*` (secret, full access). The SDK detects the `pk_*` prefix at construct time and throws a `BilliumError` immediately when a method that requires secret scope (`webhooks.create / list / update / delete / ping`, `invoices.cancel`) is called — no round-tripping a generic `403` from the backend to find out you used the wrong key. Public keys are reserved for future browser-side SDKs.
17
+ - **Prefixed resource IDs.** Every entity returned by the API has a typed prefix: `mer_` (merchant), `inv_` (invoice), `pay_` (payment), `cus_` (customer), `prd_` (product), `wh_` (webhook endpoint), `wal_` (wallet), `tle_` (invoice timeline entry), `evt_` (webhook event). Format: `{prefix}_{32 hex chars}`. The SDK treats IDs as opaque strings — pass them through verbatim. See the README's "About resource IDs" section.
18
+ - **Webhook event types.** The full `WebhookEventType` union covers every event the backend emits: `invoice.created/updated/paid/underpaid/overpaid/expired/cancelled`, `payment.created/updated/detected/confirmed/paid/underpaid/overpaid/expired`, plus the `invoice.*` and `payment.*` wildcards.
19
+ - **Per-event delivery guarantees** documented in the README. Terminal-state events (`invoice.paid`, `payment.confirmed`, etc.) flow through a transactional outbox with at-least-once delivery and crash recovery. Best-effort events (`invoice.updated`, `payment.updated`, `payment.created`) are emitted in-process for sub-second UI sync — use them as UI hints, not for critical business logic.
20
+ - **`User-Agent` header** (`billium-node/<version> (node/<process.version>)`) on every request, so backend operators can segment traffic by SDK version.
21
+ - **Dual ESM / CJS build** with TypeScript declarations for both module formats. `Billium` is exposed as a named export only — `import { Billium } from '@billium/node'` (ESM) or `const { Billium } = require('@billium/node')` (CJS) — to keep CJS consumers free of `.default` foot-guns and to tree-shake cleanly in modern bundlers.
22
+ - **Zero runtime dependencies.** The SDK uses only native Node.js APIs: `crypto` for HMAC verification, `fetch` for HTTP. Total install footprint ≈ 47 KB packed.
23
+ - **Public TypeScript surface** re-exports `Invoice`, `InvoiceStatus`, `InvoiceCustomer`, `InvoiceProduct`, `InvoicePayment`, `InvoiceTimelineEntry`, `CreateInvoiceParams`, `CreateInvoiceOptions`, `ListInvoicesParams`, `PaginatedResult`, `Webhook`, `WebhookEvent`, `WebhookEventType`, `WebhookSecret`, `CreateWebhookParams`, `UpdateWebhookParams`, `VerifyOptions`, `BilliumError`, `BilliumApiError`, `BilliumWebhookSignatureError`, `BilliumWebhookTimestampError`, and `SDK_VERSION`.
24
+ - **Test suite typecheck.** `npm run lint` typechecks both `src/` and `tests/` (via a separate `tsconfig.test.json`), so type drift in fixtures fails CI alongside source code.
25
+ - **npm provenance.** Releases are published from GitHub Actions via OIDC, so every published version carries a verifiable cryptographic attestation tying the tarball to its source commit. Verify with `npm audit signatures @billium/node`.
26
+
27
+ ### Server-side prerequisites
28
+
29
+ These backend behaviors are required for `@billium/node 1.0.0` to work end-to-end. They shipped alongside the SDK as part of the same launch:
30
+
31
+ - `POST /merchants/merchant/:merchantId/invoices` returns the full invoice DTO with relations (the same shape as `GET /invoices/:id`).
32
+ - `POST /merchants/merchant/:merchantId/invoices/:invoiceId/cancel` returns the same full shape.
33
+ - `invoice.cancelled` webhook event is emitted via the outbox when a merchant cancels an invoice, with at-least-once delivery semantics.
34
+ - `Idempotency-Key` header is honored on `POST /invoices` with a 24 h TTL, body-hash mismatch detection, and an in-flight processing lock.
35
+ - A global response interceptor (`IdSerializerInterceptor`) prefixes resource IDs on every API response (`mer_`, `inv_`, etc.), and a global request middleware (`StripIdPrefixMiddleware`) strips the prefix on the way back in. Prisma still stores bare UUIDs at the database layer — the prefix lives only on the wire.
36
+ - Two-tier rate limiting: a global IP-based throttler (50 req / 60 s) and a per-API-key throttler (300 req / 60 s, keyed off the `x-api-key` prefix). Both expose `X-RateLimit-Limit / Remaining / Reset` and `Retry-After` headers, which the SDK's retry loop honors automatically.
37
+
38
+ [1.0.0]: https://github.com/BilliumHQ/billium-node/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Billium
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # @billium/node
2
+
3
+ Official Node.js SDK for [Billium](https://billium.to) — non-custodial crypto payments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @billium/node
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```typescript
14
+ import { Billium } from '@billium/node';
15
+
16
+ const billium = new Billium({
17
+ apiKey: process.env.BILLIUM_API_KEY, // sk_... (secret key)
18
+ merchantId: process.env.BILLIUM_MERCHANT_ID, // mer_...
19
+ webhookSecret: process.env.BILLIUM_WEBHOOK_SECRET, // whsec_... (optional)
20
+ });
21
+ ```
22
+
23
+ ### About API keys
24
+
25
+ Billium issues two key types from the dashboard (Settings → Developer → API keys):
26
+
27
+ | Type | Prefix | Scope | Where to use |
28
+ |---|---|---|---|
29
+ | **Secret** | `sk_*` | Full server-side access — invoices, webhook management, customers, products | **This SDK.** Server code only — never ship a secret key to a browser. |
30
+ | **Public** | `pk_*` | Limited to `invoice.create`, `invoice.view`, `product.view` | Future browser-side SDKs (vanilla JS, React, Vue, Next.js client components) — not consumed by `@billium/node`. |
31
+
32
+ `@billium/node` is built for server environments and consumes **secret keys** (`sk_*`). If you pass a public key by mistake, methods that require secret scope (`webhooks.create()`, `invoices.cancel()`, etc.) will throw a `BilliumError` immediately with a clear message — they won't round-trip a generic `403` from the backend.
33
+
34
+ If you need to call Billium from a browser, route your requests through your own backend (running `@billium/node`) instead of calling Billium directly from client code. A browser-targeted SDK is on the roadmap, but its exact form — vanilla JS, React components, Vue, framework-agnostic — hasn't been decided yet, so there's no specific package name to wait on.
35
+
36
+ ### About resource IDs
37
+
38
+ Every Billium resource ID is prefixed with a short tag indicating its type, followed by 32 hexadecimal characters. The prefix is for human and log-debuggability — when you see one of these in an error message, you instantly know what kind of resource it points at, without having to chase down which field it came from.
39
+
40
+ | Resource | Prefix | Example |
41
+ |---|---|---|
42
+ | Merchant | `mer_` | `mer_550e8400e29b41d4a716446655440000` |
43
+ | Invoice | `inv_` | `inv_7d9b8e2c1a4f4e3d9c2b8f7a6d5e3b1c` |
44
+ | Payment | `pay_` | `pay_3a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d` |
45
+ | Customer | `cus_` | `cus_a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7` |
46
+ | Product | `prd_` | `prd_b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8` |
47
+ | Webhook endpoint | `wh_` | `wh_c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9` |
48
+ | Wallet | `wal_` | `wal_d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0` |
49
+ | Invoice timeline entry | `tle_` | `tle_e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1` |
50
+ | Webhook event | `evt_` | `evt_f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2` |
51
+
52
+ The SDK treats IDs as opaque strings. You don't need to parse or construct them yourself — pass them through verbatim. The backend accepts both prefixed and bare UUID forms during the transition window, but you should always use the prefixed form returned by the API in production code.
53
+
54
+ ## Invoices
55
+
56
+ ### Create an invoice
57
+
58
+ ```typescript
59
+ import { randomUUID } from 'crypto';
60
+
61
+ const invoice = await billium.invoices.create(
62
+ {
63
+ name: 'Order #1234',
64
+ rawAmount: 99.99,
65
+ currency: 'USD',
66
+ customerEmail: 'customer@example.com',
67
+ redirectUrl: 'https://yoursite.com/thank-you',
68
+ },
69
+ { idempotencyKey: randomUUID() },
70
+ );
71
+
72
+ console.log(invoice.id); // inv_...
73
+ ```
74
+
75
+ #### Idempotency keys
76
+
77
+ Pass `idempotencyKey` whenever you call `create()` from anywhere a retry might happen — webhook handlers, queue workers, mobile-initiated checkouts, anything subject to timeouts or duplicate clicks.
78
+
79
+ The server stores the response keyed by `(merchantId, idempotencyKey)` for **24 hours**. If the same key arrives again with the same body, you get back the original invoice — no duplicate is created. If the key arrives with a *different* body, the server returns `409 Conflict` (it's almost always a programmer bug to reuse a key for two different requests).
80
+
81
+ The key also unlocks **automatic retries** on `create()`: without it, the SDK refuses to retry a failed `POST` because it can't prove the original didn't already succeed server-side. With it, the SDK will retry on transient errors (`5xx`, `429`, network failures) using exponential backoff with jitter.
82
+
83
+ ```typescript
84
+ // One key per logical operation. UUID v4 is a good default.
85
+ await billium.invoices.create(params, { idempotencyKey: randomUUID() });
86
+
87
+ // Or, scope by your own business identifier — anything stable per attempt.
88
+ await billium.invoices.create(params, { idempotencyKey: `cart-${cartId}` });
89
+ ```
90
+
91
+ ### Get an invoice
92
+
93
+ ```typescript
94
+ const invoice = await billium.invoices.get('inv_...');
95
+
96
+ invoice.id; // 'inv_...'
97
+ invoice.status; // 'AWAITING_PAYMENT' | 'PAID' | ...
98
+ invoice.rawAmount; // string — Decimal(15,6) serialized, use a decimal lib for math
99
+ invoice.customer?.email; // string | undefined
100
+ invoice.payments; // InvoicePayment[] — on-chain payments received against this invoice
101
+ invoice.invoiceTimeline; // InvoiceTimelineEntry[] — status transition history
102
+ ```
103
+
104
+ > **Note on amounts:** `rawAmount` and `endAmount` are returned as **strings**, not numbers. They're stored as `Decimal(15, 6)` in the database and serialized as strings to preserve precision. Use a decimal library (e.g. [`decimal.js`](https://github.com/MikeMcl/decimal.js/)) if you need to do arithmetic on them.
105
+
106
+ ### List invoices
107
+
108
+ ```typescript
109
+ const result = await billium.invoices.list({
110
+ page: 1,
111
+ limit: 20,
112
+ search: 'Order',
113
+ });
114
+
115
+ console.log(result.data); // Invoice[]
116
+ console.log(result.total); // total count
117
+ ```
118
+
119
+ ### Cancel an invoice
120
+
121
+ ```typescript
122
+ await billium.invoices.cancel('inv_...');
123
+ ```
124
+
125
+ ## Webhooks
126
+
127
+ ### Verify a webhook signature
128
+
129
+ Use `billium.webhooks.verify()` inside your webhook handler to validate that the request came from Billium.
130
+
131
+ ```typescript
132
+ import express from 'express';
133
+
134
+ const app = express();
135
+
136
+ app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
137
+ try {
138
+ const event = billium.webhooks.verify(
139
+ req.body, // raw body (Buffer or string)
140
+ req.headers['x-signature'], // signature header
141
+ );
142
+
143
+ switch (event.event) {
144
+ case 'invoice.paid':
145
+ // handle payment
146
+ break;
147
+ case 'invoice.expired':
148
+ // handle expiration
149
+ break;
150
+ }
151
+
152
+ res.sendStatus(200);
153
+ } catch (err) {
154
+ console.error('Webhook verification failed:', err);
155
+ res.sendStatus(400);
156
+ }
157
+ });
158
+ ```
159
+
160
+ You can also pass the secret explicitly per call:
161
+
162
+ ```typescript
163
+ const event = billium.webhooks.verify(body, signature, 'whsec_...');
164
+ ```
165
+
166
+ ### Manage webhook endpoints
167
+
168
+ ```typescript
169
+ // Create
170
+ const webhook = await billium.webhooks.create({
171
+ url: 'https://yoursite.com/webhooks',
172
+ events: ['invoice.paid', 'invoice.expired'],
173
+ description: 'Production webhook',
174
+ });
175
+
176
+ // List
177
+ const webhooks = await billium.webhooks.list();
178
+
179
+ // Update
180
+ await billium.webhooks.update(webhook.id, {
181
+ events: ['invoice.*'],
182
+ });
183
+
184
+ // Ping (send a test event)
185
+ await billium.webhooks.ping(webhook.id);
186
+
187
+ // Delete
188
+ await billium.webhooks.delete(webhook.id);
189
+ ```
190
+
191
+ ### Webhook event types
192
+
193
+ | Event | Delivery | Description |
194
+ |-------|----------|-------------|
195
+ | `invoice.*` | — | All invoice events (subscribe wildcard) |
196
+ | `invoice.created` | durable | Invoice was created |
197
+ | `invoice.updated` | best-effort | Invoice fields changed (status, expiry, etc.) |
198
+ | `invoice.paid` | **durable** | Invoice fully paid |
199
+ | `invoice.underpaid` | **durable** | Payment received but insufficient |
200
+ | `invoice.overpaid` | **durable** | Payment exceeds invoice amount |
201
+ | `invoice.expired` | **durable** | Invoice expired without payment |
202
+ | `invoice.cancelled` | **durable** | Invoice was cancelled by the merchant |
203
+ | `payment.*` | — | All payment events (subscribe wildcard) |
204
+ | `payment.created` | best-effort | Payment was created (customer initiated checkout) |
205
+ | `payment.updated` | best-effort | Payment fields changed (e.g. confirmation count) |
206
+ | `payment.detected` | **durable** | On-chain payment detected |
207
+ | `payment.confirmed` | **durable** | Payment confirmed on-chain |
208
+ | `payment.paid` | **durable** | Payment completed |
209
+ | `payment.underpaid` | **durable** | Underpayment detected |
210
+ | `payment.overpaid` | **durable** | Overpayment detected |
211
+ | `payment.expired` | **durable** | Payment expired |
212
+
213
+ #### Delivery guarantees
214
+
215
+ Billium emits webhooks via two paths depending on the event criticality:
216
+
217
+ - **Durable events** are written to a transactional outbox in the same database transaction as the underlying state change. A background processor picks them up every 10 seconds and delivers them — **even if the Billium backend crashes between the state change and the delivery attempt**, the event is replayed once the process recovers. These events have **at-least-once** delivery semantics: design your handler to be idempotent (e.g. dedupe on the `event.id` field).
218
+
219
+ - **Best-effort events** (`invoice.updated`, `payment.updated`, `payment.created`) are emitted in-process from the same request that triggered them, optimized for real-time UI sync (sub-second latency). These events have **at-most-once** semantics: a backend crash between the state change and HTTP delivery may drop them. Use them to keep your dashboards fresh, **not** to drive critical business logic — for that, listen to the matching durable event (e.g. use `payment.detected` / `payment.confirmed` instead of `payment.updated`).
220
+
221
+ In practice: **subscribe to terminal-state events for anything that touches money or fulfillment**, and treat `*.updated` and `payment.created` as nice-to-have UI hints.
222
+
223
+ ## Configuration
224
+
225
+ ```typescript
226
+ const billium = new Billium({
227
+ apiKey: '...', // Required for invoices and webhook management
228
+ merchantId: '...', // Required for invoices and webhook management
229
+ webhookSecret: '...', // Optional — default secret for webhook verification
230
+ baseUrl: '...', // Optional — defaults to https://api.billium.to
231
+
232
+ // Retry configuration (all optional)
233
+ maxRetries: 2, // Total HTTP calls = maxRetries + 1. Default: 2
234
+ baseDelayMs: 500, // Initial backoff delay. Default: 500ms
235
+ maxDelayMs: 30_000, // Cap on backoff. Default: 30s
236
+ });
237
+ ```
238
+
239
+ ### Retries
240
+
241
+ The SDK automatically retries failed requests on:
242
+
243
+ - **Network errors** (DNS failure, connection reset, TLS handshake)
244
+ - **5xx responses** (500, 502, 503, 504)
245
+ - **429 Too Many Requests** — honoring the `Retry-After` header when present
246
+
247
+ Backoff is exponential with full jitter, so a fleet of clients failing simultaneously won't all retry at the same instant.
248
+
249
+ **Retry safety on POST**: `GET`, `PUT`, `PATCH`, and `DELETE` are always retried because they're idempotent by HTTP convention. `POST` is **only** retried when an `Idempotency-Key` is set on the request — otherwise a retry could create a duplicate resource if the original POST reached the server but the response was lost in transit. See the next section for how to set an idempotency key.
250
+
251
+ You can create a client with only `webhookSecret` if you only need to verify webhooks:
252
+
253
+ ```typescript
254
+ const billium = new Billium({
255
+ webhookSecret: process.env.BILLIUM_WEBHOOK_SECRET,
256
+ });
257
+
258
+ // This works:
259
+ const event = billium.webhooks.verify(body, signature);
260
+
261
+ // This throws — apiKey and merchantId are required:
262
+ await billium.invoices.list();
263
+ ```
264
+
265
+ ## Error handling
266
+
267
+ ```typescript
268
+ import {
269
+ BilliumError,
270
+ BilliumApiError,
271
+ BilliumWebhookSignatureError,
272
+ BilliumWebhookTimestampError,
273
+ } from '@billium/node';
274
+
275
+ try {
276
+ await billium.invoices.create({ name: 'Test', rawAmount: 10 });
277
+ } catch (err) {
278
+ if (err instanceof BilliumApiError) {
279
+ console.log(err.status); // HTTP status code
280
+ console.log(err.code); // API error code
281
+ console.log(err.message); // Error message
282
+ }
283
+ }
284
+ ```
285
+
286
+ | Error class | When |
287
+ |-------------|------|
288
+ | `BilliumError` | Base error — missing configuration |
289
+ | `BilliumApiError` | API returned a non-2xx response |
290
+ | `BilliumWebhookSignatureError` | Webhook signature mismatch or malformed header |
291
+ | `BilliumWebhookTimestampError` | Webhook timestamp outside tolerance window |
292
+
293
+ ## Requirements
294
+
295
+ - Node.js >= 18.0.0
296
+ - Zero production dependencies
297
+
298
+ ## License
299
+
300
+ MIT