@billkit-eu/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.
@@ -0,0 +1,911 @@
1
+ /**
2
+ * Opt-in logging for the BillKit SDK.
3
+ *
4
+ * A library has no business deciding where its host application's logs
5
+ * go, so this SDK ships no logger, no transport, and no destination. It
6
+ * accepts one from you and writes to a no-op until you do:
7
+ *
8
+ * ```ts
9
+ * const client = new BillKit({ logger: console });
10
+ * ```
11
+ *
12
+ * `console` satisfies {@link BillKitLogger} structurally, so that line
13
+ * works with no adapter. So does a pino/winston/bunyan child logger:
14
+ * their `debug(msg, ctx)` / `warn(msg, ctx)` signatures line up. If yours
15
+ * takes its arguments the other way round, wrap it:
16
+ *
17
+ * ```ts
18
+ * const logger = {
19
+ * debug: (m, c) => myLogger.debug(c, m),
20
+ * warn: (m, c) => myLogger.warn(c, m),
21
+ * };
22
+ * ```
23
+ *
24
+ * ## What gets logged
25
+ *
26
+ * - **debug**: one call per HTTP attempt and one per response, with
27
+ * `method`, `url`, `attempt`, `status`, `durationMs`, and `requestId`
28
+ * (quote that id to BillKit support).
29
+ * - **warn**: one call per retry, naming the reason and the delay before
30
+ * the next attempt. A retry is a real anomaly worth surfacing without
31
+ * being an error.
32
+ *
33
+ * ## What is deliberately never logged
34
+ *
35
+ * - The `Authorization` header or the API key, in any form.
36
+ * - Request and response **bodies**. They carry customer PII (emails,
37
+ * names, addresses) and billing detail; a payments SDK that quietly
38
+ * copies those into its user's log sink has manufactured a compliance
39
+ * problem on their behalf.
40
+ * - The **query string**. List filters routinely carry values like
41
+ * `email=ada@example.com`, so only the path is logged.
42
+ * - The **final failure**. Every exhausted call throws a typed
43
+ * `BillKitError` carrying the status, request id and retry-after;
44
+ * logging it here as well would produce a duplicate the caller never
45
+ * asked for and cannot suppress from their own sink.
46
+ */
47
+ /** Structured context attached to a log line. Never contains secrets. */
48
+ type LogContext = Record<string, unknown>;
49
+ /**
50
+ * The minimum a logger must do for the SDK to use it. Deliberately two
51
+ * methods: the SDK has exactly two things to say, and a narrow interface
52
+ * is one almost every logger already satisfies without an adapter.
53
+ */
54
+ interface BillKitLogger {
55
+ debug(message: string, context?: LogContext): void;
56
+ warn(message: string, context?: LogContext): void;
57
+ }
58
+ /**
59
+ * The default. Discards everything, so the SDK is silent until a logger
60
+ * is supplied, and costs nothing when it isn't.
61
+ */
62
+ declare const NOOP_LOGGER: BillKitLogger;
63
+
64
+ /**
65
+ * Retry policy for transient failures.
66
+ *
67
+ * Retries 5xx + network errors with jittered exponential backoff.
68
+ * 4xx (including 409 Idempotency-Key conflicts) are caller-fault and
69
+ * never retried. The SDK auto-generates an `Idempotency-Key` for every
70
+ * mutating call so retrying a 5xx never double-charges.
71
+ */
72
+ interface RetryPolicy {
73
+ readonly maxAttempts: number;
74
+ readonly initialBackoffMs: number;
75
+ readonly backoffMultiplier: number;
76
+ readonly maxBackoffMs: number;
77
+ readonly maxRetryAfterMs?: number;
78
+ readonly jitter: number;
79
+ }
80
+ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
81
+
82
+ /**
83
+ * Fetch-backed transport with retry + error mapping.
84
+ *
85
+ * Uses the runtime's native `fetch` (Node 20+, Bun, Deno, Cloudflare
86
+ * Workers, browsers). The transport is the only place that touches HTTP;
87
+ * everything else in the SDK speaks to a `Transport` interface so a
88
+ * caller can inject a mock or replay layer for testing.
89
+ */
90
+
91
+ type QueryValue = string | number | boolean | null | undefined;
92
+ interface RequestOptions {
93
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
94
+ path: string;
95
+ query?: {
96
+ readonly [key: string]: QueryValue;
97
+ };
98
+ body?: Record<string, unknown> | undefined;
99
+ idempotencyKey?: string | undefined;
100
+ extraHeaders?: Record<string, string>;
101
+ }
102
+ interface TransportConfig {
103
+ apiKey: string;
104
+ baseUrl?: string;
105
+ timeoutMs?: number;
106
+ retryPolicy?: RetryPolicy;
107
+ fetch?: typeof fetch;
108
+ /**
109
+ * Where to send the SDK's request/retry lifecycle. Omitted (the
110
+ * default) means a no-op: the SDK stays silent and never picks a
111
+ * destination for you. `console` works as-is; see
112
+ * {@link BillKitLogger}. Secrets, bodies and query strings are never
113
+ * passed to it.
114
+ */
115
+ logger?: BillKitLogger;
116
+ }
117
+ declare class Transport {
118
+ private readonly apiKey;
119
+ private readonly baseUrl;
120
+ private readonly timeoutMs;
121
+ private readonly retryPolicy;
122
+ private readonly fetchFn;
123
+ private readonly logger;
124
+ constructor(config: TransportConfig);
125
+ request<T = unknown>(options: RequestOptions): Promise<T>;
126
+ }
127
+
128
+ /**
129
+ * Auto-pagination helper for list endpoints.
130
+ *
131
+ * The BillKit API returns Stripe-shape envelopes:
132
+ *
133
+ * { "object": "list", "data": [...], "has_more": bool }
134
+ *
135
+ * Cursor pagination is forward-only via the last item's `id` as
136
+ * `starting_after`. `paginate` walks every page and yields each row.
137
+ * Callers consume it via `for await`:
138
+ *
139
+ * for await (const customer of client.customers.iter()) {
140
+ * ...
141
+ * }
142
+ */
143
+
144
+ interface ListResponseEnvelope<T = unknown> {
145
+ readonly data?: readonly T[];
146
+ readonly has_more?: boolean;
147
+ }
148
+ interface PaginateOptions {
149
+ /** Maps to the API's `limit` parameter. `undefined` lets the
150
+ * server pick its default (10 today). */
151
+ pageSize?: number | undefined;
152
+ /** Extra filters forwarded on every page (e.g. `type` on events,
153
+ * `action` on audit logs). Values are pruned of `undefined` so
154
+ * callers can spread their full options object in. */
155
+ filters?: Readonly<Record<string, QueryValue>>;
156
+ }
157
+ type ListFn<T> = (params: {
158
+ limit?: number | undefined;
159
+ starting_after?: string | undefined;
160
+ [key: string]: QueryValue;
161
+ }) => Promise<ListResponseEnvelope<T>>;
162
+ /**
163
+ * Walk every page of `listFn` and yield each row.
164
+ *
165
+ * Three terminators, in priority order:
166
+ * 1. `has_more=false`: the server's authoritative signal (common case).
167
+ * 2. Empty `data` with `has_more=true`: shouldn't happen per the API
168
+ * contract, but if a future server bug or proxy misbehaviour
169
+ * produced it the iterator would loop forever. Belt-and-suspenders.
170
+ * 3. The last row has no `id`, so there is no cursor to advance with. The schema
171
+ * doesn't allow it today, but same defensive reasoning.
172
+ */
173
+ declare function paginate<T>(listFn: ListFn<T>, options?: PaginateOptions): AsyncIterableIterator<T>;
174
+
175
+ /**
176
+ * Resource accessors mirroring the BillKit API surface.
177
+ *
178
+ * Each resource exposes the public verbs from `/v1/<resource>`. The
179
+ * return type defaults to `unknown`; the SDK doesn't ship runtime
180
+ * schemas (zod / valibot) because the API is Stripe-shape and tenants
181
+ * typically forward the JSON through their own data layer unchanged.
182
+ * Callers who want strong types parameterise each call with their
183
+ * own generic:
184
+ *
185
+ * interface Customer { id: string; email: string }
186
+ * const c = await client.customers.create<Customer>({ email: "..." });
187
+ *
188
+ * Every list-returning resource also exposes an `iter()` method that
189
+ * walks every page via the Stripe-shape `has_more` + `starting_after`
190
+ * cursor protocol. Iterate with `for await`:
191
+ *
192
+ * for await (const customer of client.customers.iter()) { ... }
193
+ */
194
+
195
+ /**
196
+ * Cursor-pagination knobs shared by every `list()` method.
197
+ *
198
+ * The index signature is what lets a resource-specific extension
199
+ * (e.g. `EventsListParams` adds `type?: string`) flow through the
200
+ * Transport's `query` shape without a cast. Excess fields are
201
+ * tolerated; `undefined` values are pruned before serialisation.
202
+ */
203
+ interface BaseListParams {
204
+ limit?: number;
205
+ starting_after?: string;
206
+ ending_before?: string;
207
+ readonly [key: string]: QueryValue;
208
+ }
209
+ /**
210
+ * `prices.list` params. Adds the server-side `product_id` filter on top of
211
+ * the usual cursor knobs. `GET /v1/prices?product_id=...` narrows to one
212
+ * product's prices, which beats listing everything and filtering client-side
213
+ * once a tenant has more than a page of prices.
214
+ */
215
+ interface PricesListParams extends BaseListParams {
216
+ product_id?: string;
217
+ }
218
+ /** Optional idempotency knob carried by every mutating call. */
219
+ interface IdempotencyOptions {
220
+ /** Coalesces retries across process restarts. The SDK generates
221
+ * a random `sdk-<uuid>` key for every mutating call if you don't
222
+ * supply one. Pass your own when you want retries from a different
223
+ * process to converge on the same server-side result. */
224
+ idempotencyKey?: string;
225
+ }
226
+ type ListParams = BaseListParams & {
227
+ readonly [key: string]: QueryValue;
228
+ };
229
+ interface CreateCustomerParams extends IdempotencyOptions {
230
+ email?: string;
231
+ name?: string;
232
+ country_code?: string;
233
+ metadata?: Record<string, string>;
234
+ }
235
+ interface UpdateCustomerParams extends IdempotencyOptions {
236
+ email?: string;
237
+ name?: string;
238
+ country_code?: string;
239
+ metadata?: Record<string, string>;
240
+ }
241
+ /**
242
+ * Body for `POST /v1/customers/{id}/vat_number`. The VAT number is
243
+ * sent through VIES server-side; the response carries
244
+ * `vat_number_validated` reflecting the outcome.
245
+ */
246
+ interface SetCustomerVatNumberParams extends IdempotencyOptions {
247
+ vat_number: string;
248
+ country_code?: string;
249
+ }
250
+ /**
251
+ * Body for `POST /v1/customers/{id}/purge`. The server requires
252
+ * `confirmed: true` as a fat-finger guard against accidental purges
253
+ * fired from a DELETE that meant to soft-delete. The SDK defaults
254
+ * `confirmed` to `true` so the caller doesn't have to opt in twice.
255
+ */
256
+ interface PurgeCustomerParams extends IdempotencyOptions {
257
+ confirmed?: boolean;
258
+ }
259
+ interface CreateProductParams extends IdempotencyOptions {
260
+ /** Customer-facing name, for example "Pro" or "Enterprise". */
261
+ name: string;
262
+ /** Optional long-form description shown in your own catalog UI. */
263
+ description?: string;
264
+ /** Ordered bullets suitable for pricing tables and checkout pages. */
265
+ marketing_features?: string[];
266
+ /** Small string metadata map echoed back on the Product object. */
267
+ metadata?: Record<string, string>;
268
+ }
269
+ interface UpdateProductParams extends IdempotencyOptions {
270
+ name?: string;
271
+ description?: string;
272
+ marketing_features?: string[];
273
+ metadata?: Record<string, string>;
274
+ /** Set false to stop selling a product without deleting history. */
275
+ active?: boolean;
276
+ }
277
+ interface CreatePriceParams extends IdempotencyOptions {
278
+ /** Existing Product id returned from `client.products.create`. */
279
+ product_id: string;
280
+ amount_cents: number;
281
+ currency: string;
282
+ interval: "month" | "year" | (string & {});
283
+ metadata?: Record<string, string>;
284
+ trial_days?: number;
285
+ trial_verification_cents?: number;
286
+ payment_methods?: Array<"creditcard" | "directdebit" | (string & {})>;
287
+ /**
288
+ * Per-Price refund-window override (`POST /v1/prices`). `undefined`
289
+ * inherits the default policy table (7d / 30d initial, 3d renewal);
290
+ * `0` disables refunds for that charge type; `N > 0` is an N-day
291
+ * window (capped server-side at 365). Useful for "Pro Bundle has a
292
+ * 14-day money back" or "Lifetime: no refunds" product decisions.
293
+ */
294
+ refund_window_initial_days?: number;
295
+ refund_window_renewal_days?: number;
296
+ /**
297
+ * Whether `amount_cents` is quoted gross (`"inclusive"`, VAT is
298
+ * backed out of it) or net (`"exclusive"`, VAT is added on top at
299
+ * charge time). `undefined` inherits `"unspecified"`, which defers to
300
+ * the tax rate configured for the buyer's country. Set it explicitly
301
+ * when the amount you advertise has to be the amount charged,
302
+ * regardless of what tax rates exist now or later.
303
+ */
304
+ tax_behavior?: "inclusive" | "exclusive" | "unspecified";
305
+ }
306
+ interface CreateCheckoutSessionParams extends IdempotencyOptions {
307
+ /**
308
+ * Existing Customer to attach the session to. Mutually exclusive
309
+ * with `customer_email`; exactly one of the two must be set.
310
+ */
311
+ customer_id?: string;
312
+ /**
313
+ * Stripe-compatible shortcut: BillKit creates a fresh Customer row
314
+ * in the same transaction as the checkout. Never dedupes by email
315
+ * (emails are not unique identifiers in BillKit). Mutually exclusive
316
+ * with `customer_id`.
317
+ */
318
+ customer_email?: string;
319
+ /**
320
+ * Optional friendly name carried onto the auto-created Customer when
321
+ * using `customer_email`. Rejected with `422` if supplied alongside
322
+ * `customer_id` (rename existing customers via `customers.update`).
323
+ */
324
+ customer_name?: string;
325
+ price_id: string;
326
+ success_url: string;
327
+ cancel_url: string;
328
+ /**
329
+ * Pin the Mollie payment method. `undefined` lets Mollie pick from
330
+ * the customer's available methods; when set, must be in the price's
331
+ * `payment_methods` allowlist.
332
+ */
333
+ method?: "creditcard" | "directdebit" | (string & {});
334
+ /** Optional coupon code applied at checkout; atomically claimed. */
335
+ coupon_code?: string;
336
+ /**
337
+ * Per-session trial override. Replaces the price's `trial_days`
338
+ * for this checkout. Capped server-side at `2 × max(price.trial_days, 14)`.
339
+ * `0` disables a trial that the price would otherwise grant.
340
+ */
341
+ trial_days_override?: number;
342
+ }
343
+ interface CreateRefundParams extends IdempotencyOptions {
344
+ payment_id?: string;
345
+ subscription_id?: string;
346
+ /**
347
+ * Refund a mandate-less one-shot payment (`oneShotPayments.create`).
348
+ * Mutually exclusive with `payment_id` / `subscription_id`; pass
349
+ * exactly one target or the server rejects with `400`.
350
+ */
351
+ one_shot_payment_id?: string;
352
+ /**
353
+ * Partial-refund amount in minor units. Omit to refund the whole remaining
354
+ * balance (the full charge when nothing has been refunded yet). A payment
355
+ * may carry several partial refunds up to the charged amount; the one that
356
+ * brings the cumulative total to the full charge cancels the bound
357
+ * subscription (or flips a one-shot to `refunded`).
358
+ */
359
+ amount_cents?: number;
360
+ reason?: string;
361
+ }
362
+ /**
363
+ * Body for `POST /v1/checkout/one_shot`, a single mandate-less charge
364
+ * (the Stripe PaymentIntent shape, mapped onto Mollie). No subscription,
365
+ * no mandate, no renewals: it settles once against your `success_url`.
366
+ */
367
+ interface CreateOneShotPaymentParams extends IdempotencyOptions {
368
+ /** Existing Customer to charge. */
369
+ customer_id: string;
370
+ amount_cents: number;
371
+ /** ISO-4217, e.g. `"EUR"`. Validated against the tenant allowlist. */
372
+ currency: string;
373
+ /**
374
+ * Concrete Mollie method to charge with. Required, because a one-shot commits
375
+ * up front). Validated against the tenant's capability allowlist for
376
+ * `currency`; one-off methods like `bancontact`/`eps` are allowed here
377
+ * even though they can't back a subscription.
378
+ *
379
+ * `giropay` was removed: the scheme shut down at the end of 2024 and the
380
+ * server now 422s it. The `(string & {})` tail keeps this open on
381
+ * purpose: unlike the console's read-side `PaymentMethodKind`, this is a
382
+ * *request* type the server validates, so an SDK that lags a newly-added
383
+ * method should not be the thing that blocks the call.
384
+ */
385
+ method: "creditcard" | "directdebit" | "ideal" | "bancontact" | "eps" | (string & {});
386
+ /** Where Mollie returns the payer after the hosted checkout. */
387
+ success_url: string;
388
+ /** Optional page for an abandoned/cancelled payment. */
389
+ cancel_url?: string;
390
+ /** Shown on the Mollie page + the payer's bank statement. */
391
+ description?: string;
392
+ /**
393
+ * Per-payment refund-window override in days. `undefined` inherits the
394
+ * one-shot default (30 days); `0` disables refunds for this payment;
395
+ * `N > 0` is an N-day window (capped server-side at 365).
396
+ */
397
+ refund_window_days?: number;
398
+ /**
399
+ * Whether `amount_cents` is quoted gross or net.
400
+ *
401
+ * `"inclusive"` (the default when omitted) charges `amount_cents` and
402
+ * backs the VAT out of it. `"exclusive"` reads it as a net figure and
403
+ * charges the payer `amount_cents + tax`, so the response's
404
+ * `amount_cents` comes back *larger* than the one you sent, because it
405
+ * is always what was actually charged. Reconcile against `net_cents` /
406
+ * `tax_cents` on the response.
407
+ *
408
+ * Omit to inherit the country default from your configured tax rate.
409
+ */
410
+ tax_behavior?: "inclusive" | "exclusive";
411
+ metadata?: Record<string, string>;
412
+ }
413
+ interface CreateWebhookEndpointParams extends IdempotencyOptions {
414
+ url: string;
415
+ enabled_events?: string[];
416
+ description?: string;
417
+ }
418
+ interface UpdateWebhookEndpointParams extends IdempotencyOptions {
419
+ url?: string;
420
+ enabled_events?: string[];
421
+ description?: string;
422
+ status?: string;
423
+ }
424
+ interface EventsListParams extends BaseListParams {
425
+ /** Server-side filter, e.g. `customer.created`. */
426
+ type?: string;
427
+ }
428
+ interface SetPortalBrandingParams extends IdempotencyOptions {
429
+ business_name?: string;
430
+ support_email?: string;
431
+ logo_url?: string;
432
+ theme?: Record<string, unknown>;
433
+ capabilities?: Record<string, unknown>;
434
+ }
435
+ interface RotateProviderCredentialParams extends IdempotencyOptions {
436
+ /** New raw provider API key. Encrypted server-side; never logged. */
437
+ api_key: string;
438
+ /** Defaults to the calling key's mode. */
439
+ mode?: "test" | "live" | (string & {});
440
+ /** Currently only `"mollie"`. */
441
+ provider?: "mollie" | (string & {});
442
+ }
443
+ interface CreateCouponParams extends IdempotencyOptions {
444
+ code: string;
445
+ discount_type: "percentage" | "amount" | (string & {});
446
+ discount_value: number;
447
+ duration: "once" | "repeating" | "forever" | (string & {});
448
+ duration_in_months?: number;
449
+ max_redemptions?: number;
450
+ redeem_by?: number;
451
+ applies_to_price_ids?: string[];
452
+ min_amount_cents?: number;
453
+ }
454
+ interface UpdateCouponParams extends IdempotencyOptions {
455
+ active?: boolean;
456
+ max_redemptions?: number;
457
+ redeem_by?: number;
458
+ applies_to_price_ids?: string[];
459
+ min_amount_cents?: number;
460
+ }
461
+ interface ValidateCouponParams {
462
+ code: string;
463
+ /**
464
+ * Scope the dry-run to one price. Optional: omit to validate the code
465
+ * on its own (existence, active, not exhausted, not expired). Supply it
466
+ * to also check the coupon's `applies_to_price_ids` restriction.
467
+ */
468
+ price_id?: string;
469
+ /**
470
+ * Base amount the discount is computed against, in minor units.
471
+ * Optional: omit to skip the discount math and the `min_amount_cents`
472
+ * check. `POST /v1/coupons/validate` treats both fields as nullable.
473
+ */
474
+ amount_cents?: number;
475
+ }
476
+ interface CreateTaxRateParams extends IdempotencyOptions {
477
+ country_code: string;
478
+ rate_basis_points: number;
479
+ display_name?: string;
480
+ inclusive?: boolean;
481
+ }
482
+ interface UpdateTaxRateParams extends IdempotencyOptions {
483
+ rate_basis_points?: number;
484
+ display_name?: string;
485
+ inclusive?: boolean;
486
+ active?: boolean;
487
+ }
488
+ interface AuditLogsListParams extends BaseListParams {
489
+ action?: string;
490
+ resource_type?: string;
491
+ actor_id?: string;
492
+ }
493
+ interface CreateBillingPortalSessionParams extends IdempotencyOptions {
494
+ subscription_id: string;
495
+ return_url: string;
496
+ }
497
+ /**
498
+ * Shared transport wrapper. Resources subclass this so each method
499
+ * reads as a single line, "verb to path with params", instead of
500
+ * the four-line `this.t.request({ ... })` boilerplate the previous
501
+ * draft repeated everywhere.
502
+ */
503
+ declare abstract class BaseResource {
504
+ protected readonly t: Transport;
505
+ constructor(t: Transport);
506
+ protected get<T>(path: string, query?: BaseListParams & Record<string, QueryValue>): Promise<T>;
507
+ protected post<T, P extends IdempotencyOptions>(path: string, params: P): Promise<T>;
508
+ /** POST with no body, used by lifecycle verbs (cancel, resume, revoke ...). */
509
+ protected postEmpty<T>(path: string, params?: IdempotencyOptions): Promise<T>;
510
+ /** POST with a fixed body and no idempotency stripping (used by
511
+ * endpoints whose body is fully specified by the caller's args
512
+ * and not optional, e.g. `preview_update`). */
513
+ protected postFixed<T>(path: string, body: Record<string, unknown>, params?: IdempotencyOptions): Promise<T>;
514
+ protected del<T>(path: string, params?: IdempotencyOptions): Promise<T>;
515
+ }
516
+ declare class Customers extends BaseResource {
517
+ /** Create a tenant-scoped buyer record. */
518
+ create<T = unknown>(params?: CreateCustomerParams): Promise<T>;
519
+ retrieve<T = unknown>(id: string): Promise<T>;
520
+ update<T = unknown>(id: string, params?: UpdateCustomerParams): Promise<T>;
521
+ delete<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
522
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
523
+ /** Walk every page of `list()` and yield each customer. */
524
+ iter<T = unknown>(options?: {
525
+ pageSize?: number;
526
+ }): AsyncIterableIterator<T>;
527
+ /**
528
+ * Attach or replace the customer's VAT number; triggers server-side
529
+ * VIES validation. The response carries `vat_number_validated`
530
+ * reflecting whether VIES confirmed the number.
531
+ */
532
+ setVatNumber<T = unknown>(id: string, params: SetCustomerVatNumberParams): Promise<T>;
533
+ /**
534
+ * Hard-purge a customer's PII for GDPR erasure. Distinct from
535
+ * `delete()` (soft delete): purge nulls email/name/country/VAT/
536
+ * metadata, sets `purged_at`, and is irreversible.
537
+ *
538
+ * The server requires `confirmed: true` as a fat-finger guard; the
539
+ * SDK defaults it to `true` so callers don't have to opt in twice.
540
+ */
541
+ purge<T = unknown>(id: string, params?: PurgeCustomerParams): Promise<T>;
542
+ }
543
+ declare class Products extends BaseResource {
544
+ /** Create a catalog Product, then attach one or more Prices to it. */
545
+ create<T = unknown>(params: CreateProductParams): Promise<T>;
546
+ retrieve<T = unknown>(id: string): Promise<T>;
547
+ /** Patch mutable Product fields. */
548
+ update<T = unknown>(id: string, params: UpdateProductParams): Promise<T>;
549
+ /** Archive a Product. */
550
+ delete<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
551
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
552
+ iter<T = unknown>(options?: {
553
+ pageSize?: number;
554
+ }): AsyncIterableIterator<T>;
555
+ }
556
+ declare class Prices extends BaseResource {
557
+ /** Create immutable billing terms for an existing Product. */
558
+ create<T = unknown>(params: CreatePriceParams): Promise<T>;
559
+ retrieve<T = unknown>(id: string): Promise<T>;
560
+ list<T = unknown>(params?: PricesListParams): Promise<ListResponseEnvelope<T>>;
561
+ iter<T = unknown>(options?: {
562
+ pageSize?: number;
563
+ product_id?: string;
564
+ }): AsyncIterableIterator<T>;
565
+ }
566
+ declare class CheckoutSessions extends BaseResource {
567
+ create<T = unknown>(params: CreateCheckoutSessionParams): Promise<T>;
568
+ retrieve<T = unknown>(id: string): Promise<T>;
569
+ }
570
+ /**
571
+ * Mandate-less one-shot payments (`/v1/checkout/one_shot`).
572
+ *
573
+ * A one-shot is the Stripe PaymentIntent shape mapped onto Mollie: a
574
+ * single `sequenceType=oneoff` charge that provisions nothing: no
575
+ * subscription, no mandate, no renewals. Drive terminal state via the
576
+ * `one_shot_payment.succeeded` / `.failed` webhook events; refund one
577
+ * with `client.refunds.create({ one_shot_payment_id })`.
578
+ */
579
+ declare class OneShotPayments extends BaseResource {
580
+ /** Create a one-off charge; returns the object with a `redirect_url`. */
581
+ create<T = unknown>(params: CreateOneShotPaymentParams): Promise<T>;
582
+ retrieve<T = unknown>(id: string): Promise<T>;
583
+ }
584
+ declare class Subscriptions extends BaseResource {
585
+ retrieve<T = unknown>(id: string): Promise<T>;
586
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
587
+ iter<T = unknown>(options?: {
588
+ pageSize?: number;
589
+ }): AsyncIterableIterator<T>;
590
+ cancel<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
591
+ pause<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
592
+ resume<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
593
+ /**
594
+ * Reactivate a canceled-but-still-in-period subscription.
595
+ *
596
+ * Distinct from `resume()` (paused → active): `reactivate()` flips
597
+ * `canceled` back to `active` for the remainder of the current
598
+ * period, so the customer keeps service without a new checkout.
599
+ * Returns `409` if the period has already elapsed.
600
+ */
601
+ reactivate<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
602
+ previewUpdate<T = unknown>(id: string, params: {
603
+ target_price_id: string;
604
+ }): Promise<T>;
605
+ update<T = unknown>(id: string, params: {
606
+ target_price_id: string;
607
+ } & IdempotencyOptions): Promise<T>;
608
+ reauthorizePaymentMethod<T = unknown>(id: string, params: {
609
+ return_url: string;
610
+ } & IdempotencyOptions): Promise<T>;
611
+ }
612
+ declare class Refunds extends BaseResource {
613
+ create<T = unknown>(params: CreateRefundParams): Promise<T>;
614
+ retrieve<T = unknown>(id: string): Promise<T>;
615
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
616
+ iter<T = unknown>(options?: {
617
+ pageSize?: number;
618
+ }): AsyncIterableIterator<T>;
619
+ }
620
+ /**
621
+ * Chargebacks / disputes (`/v1/disputes`).
622
+ *
623
+ * Read-only. Disputes are provider-originated (opened by the cardholder's
624
+ * bank) and surfaced via the `dispute.created` / `dispute.closed` webhook
625
+ * events. There is no create/update. A dispute's `status` is `open` or `won`
626
+ * (chargeback reversed); Mollie exposes no "lost" signal, so an upheld
627
+ * chargeback stays `open` (treat any non-`won` dispute as unresolved).
628
+ */
629
+ declare class Disputes extends BaseResource {
630
+ retrieve<T = unknown>(id: string): Promise<T>;
631
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
632
+ iter<T = unknown>(options?: {
633
+ pageSize?: number;
634
+ }): AsyncIterableIterator<T>;
635
+ }
636
+ declare class WebhookEndpoints extends BaseResource {
637
+ create<T = unknown>(params: CreateWebhookEndpointParams): Promise<T>;
638
+ retrieve<T = unknown>(id: string): Promise<T>;
639
+ update<T = unknown>(id: string, params: UpdateWebhookEndpointParams): Promise<T>;
640
+ delete<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
641
+ /** Rotate the signing secret. The new `whsec_...` is returned once. */
642
+ rotateSecret<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
643
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
644
+ iter<T = unknown>(options?: {
645
+ pageSize?: number;
646
+ }): AsyncIterableIterator<T>;
647
+ /**
648
+ * List per-attempt delivery records for one endpoint.
649
+ *
650
+ * Useful when a tenant's receiver is failing. Surfaces the status
651
+ * code, response body excerpt, error, and next-attempt timestamp
652
+ * for each event × endpoint pair.
653
+ */
654
+ listDeliveries<T = unknown>(endpointId: string, params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
655
+ /** Walk every page of `listDeliveries()` for one endpoint. */
656
+ iterDeliveries<T = unknown>(endpointId: string, options?: {
657
+ pageSize?: number;
658
+ }): AsyncIterableIterator<T>;
659
+ /** Fetch one delivery row for inspection before deciding to redeliver. */
660
+ getDelivery<T = unknown>(endpointId: string, deliveryId: string): Promise<T>;
661
+ /**
662
+ * Re-enqueue a delivery row for the dispatcher.
663
+ *
664
+ * Idempotent: a row already in `delivered` returns unchanged. A
665
+ * `pending` / `failed` row flips to `pending` with
666
+ * `next_attempt_at = now()`; `attempt_count` is preserved.
667
+ */
668
+ redeliver<T = unknown>(endpointId: string, deliveryId: string, params?: IdempotencyOptions): Promise<T>;
669
+ }
670
+ declare class Events extends BaseResource {
671
+ retrieve<T = unknown>(id: string): Promise<T>;
672
+ list<T = unknown>(params?: EventsListParams): Promise<ListResponseEnvelope<T>>;
673
+ /** Walk every page of `list()`. Pass `type` to filter at the server. */
674
+ iter<T = unknown>(options?: {
675
+ pageSize?: number;
676
+ type?: string;
677
+ }): AsyncIterableIterator<T>;
678
+ }
679
+ /**
680
+ * Read + mutate tenant-level configuration.
681
+ *
682
+ * Exposes the Mollie capability cache, the portal-branding row, and
683
+ * the encrypted Mollie API key. None of these are per-resource;
684
+ * they're tenant-wide knobs.
685
+ */
686
+ declare class Tenant extends BaseResource {
687
+ /** Cached Mollie profile shape (enabled methods, country, currency). */
688
+ capabilities<T = unknown>(): Promise<T>;
689
+ /** Current portal branding row (business name, theme, capability flags). */
690
+ portalBranding<T = unknown>(): Promise<T>;
691
+ /**
692
+ * Partial-update the portal branding row.
693
+ *
694
+ * Only fields you set are sent. Pass `undefined` to leave a field
695
+ * untouched; sending an empty string explicitly clears it.
696
+ */
697
+ setPortalBranding<T = unknown>(params?: SetPortalBrandingParams): Promise<T>;
698
+ /**
699
+ * Rotate the encrypted provider credential for this tenant.
700
+ *
701
+ * The new `api_key` is encrypted server-side; nothing is logged.
702
+ * `mode` defaults to the calling key's mode; prefix-mismatch
703
+ * (`test_...` under live, `live_...` under test) is rejected at the
704
+ * API boundary.
705
+ */
706
+ rotateProviderCredential<T = unknown>(params: RotateProviderCredentialParams): Promise<T>;
707
+ }
708
+ declare class Coupons extends BaseResource {
709
+ create<T = unknown>(params: CreateCouponParams): Promise<T>;
710
+ retrieve<T = unknown>(id: string): Promise<T>;
711
+ update<T = unknown>(id: string, params: UpdateCouponParams): Promise<T>;
712
+ delete<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
713
+ /**
714
+ * Server-side dry-run of a coupon redemption.
715
+ *
716
+ * Returns the discount math without atomically claiming the coupon,
717
+ * which is useful for "preview before checkout" UX.
718
+ */
719
+ validate<T = unknown>(params: ValidateCouponParams): Promise<T>;
720
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
721
+ iter<T = unknown>(options?: {
722
+ pageSize?: number;
723
+ }): AsyncIterableIterator<T>;
724
+ }
725
+ declare class TaxRates extends BaseResource {
726
+ create<T = unknown>(params: CreateTaxRateParams): Promise<T>;
727
+ retrieve<T = unknown>(id: string): Promise<T>;
728
+ update<T = unknown>(id: string, params: UpdateTaxRateParams): Promise<T>;
729
+ delete<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
730
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
731
+ iter<T = unknown>(options?: {
732
+ pageSize?: number;
733
+ }): AsyncIterableIterator<T>;
734
+ }
735
+ /**
736
+ * Read-only access to generated invoices.
737
+ *
738
+ * Invoices are produced by the billing pipeline; tenants don't create
739
+ * them directly. PDF retrieval returns a 302 redirect to the storage
740
+ * adapter's signed URL. Follow it transparently with the runtime's
741
+ * fetch settings.
742
+ */
743
+ declare class Invoices extends BaseResource {
744
+ retrieve<T = unknown>(id: string): Promise<T>;
745
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
746
+ iter<T = unknown>(options?: {
747
+ pageSize?: number;
748
+ }): AsyncIterableIterator<T>;
749
+ }
750
+ /**
751
+ * Read-only access to the per-tenant audit log.
752
+ *
753
+ * Supports server-side filters: `action`, `resource_type`, `actor_id`.
754
+ * The filters are forwarded through to `iter()` so an audit walk can
755
+ * scope to a single actor or action without client-side filtering.
756
+ */
757
+ declare class AuditLogs extends BaseResource {
758
+ retrieve<T = unknown>(id: string): Promise<T>;
759
+ list<T = unknown>(params?: AuditLogsListParams): Promise<ListResponseEnvelope<T>>;
760
+ iter<T = unknown>(options?: {
761
+ pageSize?: number;
762
+ action?: string;
763
+ resource_type?: string;
764
+ actor_id?: string;
765
+ }): AsyncIterableIterator<T>;
766
+ }
767
+ /**
768
+ * Read-only access to the payment ledger.
769
+ *
770
+ * Payments are written by the billing pipeline (checkout, renewal,
771
+ * reauthorize). Inspect attempts and their Mollie-side metadata here;
772
+ * refunds and disputes are separate flows.
773
+ */
774
+ declare class Payments extends BaseResource {
775
+ retrieve<T = unknown>(id: string): Promise<T>;
776
+ list<T = unknown>(params?: BaseListParams): Promise<ListResponseEnvelope<T>>;
777
+ iter<T = unknown>(options?: {
778
+ pageSize?: number;
779
+ }): AsyncIterableIterator<T>;
780
+ }
781
+ /**
782
+ * Mint and revoke customer-facing billing-portal sessions.
783
+ *
784
+ * Each session token is scoped to a single subscription with a
785
+ * sliding 30-minute idle window and a 2-hour hard cap. The raw token
786
+ * is returned **once** on mint; the response also includes the URL
787
+ * the tenant embeds in their app.
788
+ */
789
+ declare class BillingPortalSessions extends BaseResource {
790
+ create<T = unknown>(params: CreateBillingPortalSessionParams): Promise<T>;
791
+ /** Kill an in-the-wild portal session. Idempotent. */
792
+ revoke<T = unknown>(id: string, params?: IdempotencyOptions): Promise<T>;
793
+ }
794
+
795
+ /**
796
+ * Top-level BillKit client.
797
+ *
798
+ * Wraps a single Transport and exposes every resource family as a
799
+ * field. The Transport is configured once at construction; every
800
+ * resource reuses it, so a runtime-injected `fetch` impl (Cloudflare
801
+ * Workers, MSW for tests, a debug proxy) flows through automatically.
802
+ */
803
+
804
+ interface BillKitOptions extends Omit<TransportConfig, "apiKey"> {
805
+ /** Falls back to `process.env.BILLKIT_API_KEY` when omitted. */
806
+ apiKey?: string;
807
+ }
808
+ declare class BillKit {
809
+ readonly customers: Customers;
810
+ readonly products: Products;
811
+ readonly prices: Prices;
812
+ readonly checkoutSessions: CheckoutSessions;
813
+ readonly oneShotPayments: OneShotPayments;
814
+ readonly subscriptions: Subscriptions;
815
+ readonly refunds: Refunds;
816
+ readonly disputes: Disputes;
817
+ readonly webhookEndpoints: WebhookEndpoints;
818
+ readonly events: Events;
819
+ readonly tenant: Tenant;
820
+ readonly coupons: Coupons;
821
+ readonly taxRates: TaxRates;
822
+ readonly invoices: Invoices;
823
+ readonly auditLogs: AuditLogs;
824
+ readonly payments: Payments;
825
+ readonly billingPortalSessions: BillingPortalSessions;
826
+ constructor(options?: BillKitOptions);
827
+ }
828
+
829
+ interface BillKitErrorOptions {
830
+ type?: string | undefined;
831
+ code?: string | undefined;
832
+ param?: string | undefined;
833
+ statusCode?: number | undefined;
834
+ requestId?: string | undefined;
835
+ rawBody?: unknown;
836
+ }
837
+ declare class BillKitError extends Error {
838
+ name: string;
839
+ readonly type: string | undefined;
840
+ readonly code: string | undefined;
841
+ readonly param: string | undefined;
842
+ readonly statusCode: number | undefined;
843
+ readonly requestId: string | undefined;
844
+ readonly rawBody: unknown;
845
+ constructor(message: string, options?: BillKitErrorOptions);
846
+ }
847
+ declare class APIConnectionError extends BillKitError {
848
+ name: string;
849
+ }
850
+ declare class APIError extends BillKitError {
851
+ name: string;
852
+ }
853
+ declare class ServerError extends APIError {
854
+ name: string;
855
+ }
856
+ declare class AuthenticationError extends BillKitError {
857
+ name: string;
858
+ }
859
+ declare class PermissionError extends BillKitError {
860
+ name: string;
861
+ }
862
+ declare class ResourceMissingError extends BillKitError {
863
+ name: string;
864
+ }
865
+ declare class InvalidRequestError extends BillKitError {
866
+ name: string;
867
+ }
868
+ declare class ConflictError extends BillKitError {
869
+ name: string;
870
+ }
871
+ declare class RateLimitError extends BillKitError {
872
+ name: string;
873
+ readonly retryAfter: number | undefined;
874
+ constructor(message: string, options?: BillKitErrorOptions & {
875
+ retryAfter?: number | undefined;
876
+ });
877
+ }
878
+
879
+ declare const VERSION = "0.1.0";
880
+
881
+ /**
882
+ * Verify `BillKit-Signature: t=<unix>,v1=<hex>` headers.
883
+ *
884
+ * Works in Node 20+, Bun, Deno, Cloudflare Workers and the browser:
885
+ * we use the Web Crypto API (`globalThis.crypto.subtle`) which is
886
+ * available in all modern runtimes. The verifier:
887
+ *
888
+ * 1. Parses the header (rejects malformed shapes). A header may carry
889
+ * more than one `v1=` value, because the server emits both the old and new
890
+ * signature during a signing-secret rotation, and verification passes
891
+ * if any of them matches.
892
+ * 2. Confirms the timestamp is within `toleranceSeconds` of now
893
+ * (replay protection).
894
+ * 3. Computes the expected HMAC and compares against each candidate in
895
+ * constant time.
896
+ */
897
+ declare const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
898
+ declare class WebhookVerificationError extends Error {
899
+ name: string;
900
+ constructor(message: string);
901
+ }
902
+ interface VerifyWebhookOptions {
903
+ payload: string | Uint8Array;
904
+ signatureHeader: string | null | undefined;
905
+ secret: string;
906
+ toleranceSeconds?: number;
907
+ nowMs?: number;
908
+ }
909
+ declare function verifyWebhookSignature<T = unknown>(options: VerifyWebhookOptions): Promise<T>;
910
+
911
+ export { APIConnectionError, APIError, type AuditLogsListParams, AuthenticationError, type BaseListParams, BillKit, BillKitError, type BillKitLogger, type BillKitOptions, ConflictError, type CreateBillingPortalSessionParams, type CreateCheckoutSessionParams, type CreateCouponParams, type CreateCustomerParams, type CreateOneShotPaymentParams, type CreatePriceParams, type CreateProductParams, type CreateRefundParams, type CreateTaxRateParams, type CreateWebhookEndpointParams, DEFAULT_RETRY_POLICY, DEFAULT_WEBHOOK_TOLERANCE_SECONDS, type EventsListParams, type IdempotencyOptions, InvalidRequestError, type ListParams, type ListResponseEnvelope, type LogContext, NOOP_LOGGER, type PaginateOptions, PermissionError, type PricesListParams, RateLimitError, ResourceMissingError, type RetryPolicy, type RotateProviderCredentialParams, ServerError, type SetPortalBrandingParams, type UpdateCouponParams, type UpdateCustomerParams, type UpdateProductParams, type UpdateTaxRateParams, type UpdateWebhookEndpointParams, VERSION, type ValidateCouponParams, type VerifyWebhookOptions, WebhookVerificationError, paginate, verifyWebhookSignature };