@usebillow/sdk 0.5.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +274 -0
  4. package/dist/billing-C4RIMgH_.d.ts +1053 -0
  5. package/dist/billing-DZ4rIyg7.d.cts +1053 -0
  6. package/dist/billing-status-BZQN_gm7.d.cts +29 -0
  7. package/dist/billing-status-BZQN_gm7.d.ts +29 -0
  8. package/dist/chunk-CCG4F5FK.js +48 -0
  9. package/dist/chunk-CCG4F5FK.js.map +1 -0
  10. package/dist/chunk-Z6VXPONT.js +1493 -0
  11. package/dist/chunk-Z6VXPONT.js.map +1 -0
  12. package/dist/config.cjs +233 -0
  13. package/dist/config.cjs.map +1 -0
  14. package/dist/config.d.cts +104 -0
  15. package/dist/config.d.ts +104 -0
  16. package/dist/config.js +228 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/credits-C3Fe3TO0.d.cts +315 -0
  19. package/dist/credits-C3Fe3TO0.d.ts +315 -0
  20. package/dist/index.cjs +1560 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.cts +2379 -0
  23. package/dist/index.d.ts +2379 -0
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ingestion.cjs +259 -0
  27. package/dist/ingestion.cjs.map +1 -0
  28. package/dist/ingestion.d.cts +182 -0
  29. package/dist/ingestion.d.ts +182 -0
  30. package/dist/ingestion.js +252 -0
  31. package/dist/ingestion.js.map +1 -0
  32. package/dist/react.cjs +360 -0
  33. package/dist/react.cjs.map +1 -0
  34. package/dist/react.d.cts +71 -0
  35. package/dist/react.d.ts +71 -0
  36. package/dist/react.js +153 -0
  37. package/dist/react.js.map +1 -0
  38. package/dist/server.cjs +98 -0
  39. package/dist/server.cjs.map +1 -0
  40. package/dist/server.d.cts +54 -0
  41. package/dist/server.d.ts +54 -0
  42. package/dist/server.js +96 -0
  43. package/dist/server.js.map +1 -0
  44. package/dist/status.cjs +60 -0
  45. package/dist/status.cjs.map +1 -0
  46. package/dist/status.d.cts +31 -0
  47. package/dist/status.d.ts +31 -0
  48. package/dist/status.js +3 -0
  49. package/dist/status.js.map +1 -0
  50. package/dist/webhooks.cjs +157 -0
  51. package/dist/webhooks.cjs.map +1 -0
  52. package/dist/webhooks.d.cts +391 -0
  53. package/dist/webhooks.d.ts +391 -0
  54. package/dist/webhooks.js +143 -0
  55. package/dist/webhooks.js.map +1 -0
  56. package/package.json +169 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @usebillow/sdk
2
+
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c6c114a: Prepaid credits can be sold as Credit Packs. `credits.packs.list(currency)` lists the packs a customer can buy in one currency, cheapest first, with each pack's price, tax and total (Money), its credits and bonus (decimal strings of microcredits), `pricePerCredit` and `savingsBps`; `credits.packs.create`, `get` and `update` configure them. `credits.topUps.create(input, { idempotencyKey })` buys a pack and answers the top-up with its `checkoutUrl` and `replayed` flag, `credits.topUps.get` reads one, and `credits.topUps.list(customer, { status })` pages a customer's top-ups by cursor. Webhook payload types cover the new `credit_top_up.succeeded`, `credit_top_up.failed`, `credit_top_up.refunded` and `credit_top_up.unfulfilled` events, the customer data export types carry `topUps` and `reversals`, and `reversal_exceeds_consumption` joins the error codes.
8
+ - 6bf89c8: First public release on npm as `@usebillow/sdk`, under the MIT license. Ships dual ESM and CommonJS builds with complete type declarations for every entry point: `@usebillow/sdk`, `/config`, `/server`, `/react`, `/webhooks`, `/ingestion` and `/status`.
9
+ - 6bf89c8: A client with no `baseUrl` option and no `BILLOW_URL` environment variable now talks to the hosted API at `https://api.usebillow.com`, instead of `http://localhost:4001` with a warning. Local development and self-hosted servers set `baseUrl` (or `BILLOW_URL`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DevCoat
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,274 @@
1
+ # @usebillow/sdk
2
+
3
+ The official client SDK for [Billow](https://usebillow.com) - a self-hostable billing
4
+ layer over Paymob & Stripe. Fully typed, ESM + CJS, with first-class React support.
5
+
6
+ ```bash
7
+ npm install @usebillow/sdk
8
+ ```
9
+
10
+ ## Server client
11
+
12
+ The core client holds your **secret** key and is used server-side only.
13
+
14
+ ```ts
15
+ import { Billow } from "@usebillow/sdk";
16
+
17
+ const billow = new Billow(process.env.BILLOW_SECRET_KEY!);
18
+
19
+ const allowed = await billow.check({ customerId, featureId: "projects" });
20
+ await billow.track({ customerId, featureId: "api_calls", value: 1 });
21
+ const { entitlements } = await billow.entitlements(customerId);
22
+ ```
23
+
24
+ The client talks to the hosted API, `https://api.usebillow.com`. For local development or a
25
+ self-hosted server, set the `baseUrl` option (or the `BILLOW_URL` environment variable, which the
26
+ option overrides):
27
+
28
+ ```ts
29
+ const billow = new Billow(process.env.BILLOW_SECRET_KEY!, {
30
+ baseUrl: "http://localhost:4001", // your local Billow server
31
+ });
32
+ ```
33
+
34
+ For a server-controlled off-session payment, charge the customer's active default saved card
35
+ with a required idempotency key:
36
+
37
+ ```ts
38
+ await billow.charges.createMerchantInitiated(
39
+ {
40
+ customerId,
41
+ paymentMethodId,
42
+ amount: 5_000,
43
+ currency: "EGP",
44
+ items: [{ name: "Automatic top-up", amount: 5_000, quantity: 1 }],
45
+ },
46
+ { idempotencyKey: `topup:${attemptId}` },
47
+ );
48
+ ```
49
+
50
+ Refunds take a required idempotency key too: retrying under the same key (after a timeout, say)
51
+ returns the same refund instead of refunding twice, with `replayed: true`. Pass `{}` instead of
52
+ `{ amount }` to refund the whole remaining balance:
53
+
54
+ ```ts
55
+ const refund = await billow.charges.refund(
56
+ chargeId,
57
+ { amount: 2_000 },
58
+ { idempotencyKey: `refund:${requestId}` },
59
+ );
60
+ if (refund.status === "failed") {
61
+ // The provider declined it: nothing was refunded. Ask again under a new key.
62
+ }
63
+ ```
64
+
65
+ Check `status` even when the call resolves. A refund the provider declines answers
66
+ `502 provider_error`, which throws by default; with `maxRetries` above 0 the SDK retries that 502
67
+ under the same key and resolves with the declined refund (`status: "failed"`, `replayed: true`).
68
+
69
+ Prepaid credit grants take a required idempotency key as well; a retry under it returns the same
70
+ grant with `replayed: true`. Credit quantities are decimal strings of microcredits
71
+ (1 credit = 1,000,000), never numbers - use `BigInt` for arithmetic:
72
+
73
+ ```ts
74
+ const grant = await billow.credits.grants.create(
75
+ { customerId, kind: "promotional", amount: "5000000" },
76
+ { idempotencyKey: `welcome:${customerId}` },
77
+ );
78
+ ```
79
+
80
+ Read a customer's credits back - the balance (true at its `asOf`), the ledger newest first (it
81
+ pages by cursor, so entries arriving mid-walk are never skipped or repeated), and daily usage:
82
+
83
+ ```ts
84
+ const balance = await billow.credits.balance.get(customerId);
85
+ for await (const entry of billow.credits.ledger.list(customerId)) {
86
+ // entry.type, entry.availableDelta, entry.availableAfter, ...
87
+ }
88
+ const usage = await billow.credits.usage.get(customerId, { groupBy: "category" });
89
+ ```
90
+
91
+ ## Auto-metering (LLM tokens & any usage source)
92
+
93
+ Stop hand-instrumenting usage. `@usebillow/sdk/ingestion` wraps your calls and reports
94
+ usage to `track()` for you - **server-side only** (a browser can't be trusted to
95
+ self-report) and **non-blocking** (metering can never delay or break the wrapped
96
+ call; a failed `track()` retries under an idempotency key, then drops silently).
97
+
98
+ ```ts
99
+ import { Billow } from "@usebillow/sdk";
100
+ import { createUsageMeter, meterOpenAI } from "@usebillow/sdk/ingestion";
101
+ import OpenAI from "openai";
102
+
103
+ const billow = new Billow(process.env.BILLOW_SECRET_KEY!);
104
+ const meter = createUsageMeter(billow);
105
+
106
+ // Every completion now auto-meters its token usage (read from the provider response).
107
+ const openai = meterOpenAI(new OpenAI(), {
108
+ meter,
109
+ feature: "tokens",
110
+ customer: (params) => params.user as string, // your billow customer id
111
+ });
112
+
113
+ await openai.chat.completions.create({ model: "gpt-4o", messages, user: "cus_123" });
114
+ // For a STREAMED call, add stream_options:{ include_usage:true } so tokens can be counted.
115
+ // Streamed calls are metered when consumed the normal way (await, then `for await`).
116
+ // Consuming via `.tee()` / `.toReadableStream()` / `.withResponse()` still works but
117
+ // bypasses the token tap.
118
+ ```
119
+
120
+ Vercel AI SDK - meter the result of `generateText`/`streamText` (never touches the
121
+ stream). Works with v4 and v5 usage shapes:
122
+
123
+ ```ts
124
+ import { meterAIResult } from "@usebillow/sdk/ingestion";
125
+
126
+ const result = streamText({ model, prompt });
127
+ meterAIResult(result, { meter, feature: "tokens", customer: "cus_123" });
128
+ return result.toDataStreamResponse();
129
+ ```
130
+
131
+ Any other source - wrap any async function with the generic primitive:
132
+
133
+ ```ts
134
+ import { meterFunction } from "@usebillow/sdk/ingestion";
135
+
136
+ const upload = meterFunction(putObject, meter, (res, [file]) => ({
137
+ customerId: file.ownerId,
138
+ featureId: "storage_bytes",
139
+ value: res.size,
140
+ }));
141
+ ```
142
+
143
+ In serverless handlers with a **non-streamed** response (`generateText`, a blocking
144
+ completion), `await meter.flush()` before returning so buffered records aren't lost
145
+ when the process freezes. Do **not** await `flush()` before returning a **streamed**
146
+ response - a stream's usage resolves only after the client consumes it, so blocking
147
+ on flush first would hang the response. There, flush after the stream finishes (the
148
+ AI SDK's `onFinish`, or your platform's `waitUntil`); `meterAIResult` already tracks
149
+ the pending usage so it's recorded in the background.
150
+
151
+ ## React (Next.js, Remix, …)
152
+
153
+ Mount the server handler so the browser never sees your secret key, then use the hooks.
154
+ The customer is resolved from **your** session - never chosen by the browser.
155
+
156
+ ```ts
157
+ // app/api/billow/[...billow]/route.ts
158
+ import { createBillowHandler } from "@usebillow/sdk/server";
159
+ import { billow } from "@/lib/billow";
160
+
161
+ const handler = createBillowHandler(billow, {
162
+ identify: async (req) => getUserIdFromSession(req),
163
+ });
164
+ export { handler as GET, handler as POST };
165
+ ```
166
+
167
+ ```tsx
168
+ // app/providers.tsx
169
+ "use client";
170
+ import { BillowProvider, useCheck, useEntitlements } from "@usebillow/sdk/react";
171
+
172
+ export function Providers({ children }) {
173
+ return <BillowProvider basePath="/api/billow">{children}</BillowProvider>;
174
+ }
175
+ ```
176
+
177
+ ## Pricing table (publishable key, no backend)
178
+
179
+ Read the public catalog straight from the browser with a **publishable** key
180
+ (`bl_<env>_pk_…`) - CORS is enabled for the catalog, so a pure SPA needs no backend:
181
+
182
+ ```tsx
183
+ import { usePublishableProducts } from "@usebillow/sdk/react";
184
+
185
+ function Pricing() {
186
+ const { products, isLoading } = usePublishableProducts({
187
+ publishableKey: "bl_live_pk_…",
188
+ baseUrl: "https://api.usebillow.com",
189
+ });
190
+ if (isLoading) return null;
191
+ return products.map((p) => /* render p.name + p.prices */ null);
192
+ }
193
+ ```
194
+
195
+ Publishable keys read only public product/price data. Customer entitlements and
196
+ checkout still go through the server handler (above) or a portal session.
197
+
198
+ ## Verify webhooks
199
+
200
+ Verification is async (Web Crypto) so it runs in Node, edge, and Workers alike.
201
+ Always pass the **raw** request body.
202
+
203
+ ```ts
204
+ import { constructEvent } from "@usebillow/sdk/webhooks";
205
+
206
+ const event = await constructEvent(rawBody, req.headers["billow-signature"], endpointSecret);
207
+ switch (event.type) {
208
+ case "invoice.paid": // event.data is typed
209
+ break;
210
+ }
211
+ ```
212
+
213
+ `constructEvent` throws `WebhookVerificationError` on a bad signature or a timestamp
214
+ outside the freshness window (default 5 min). Use `verifySignature(...)` for a
215
+ boolean check.
216
+
217
+ ## Config as code
218
+
219
+ ```ts
220
+ import { defineConfig, syncConfig } from "@usebillow/sdk/config";
221
+
222
+ const config = defineConfig({ features: [/* … */], products: [/* … */] });
223
+ await syncConfig(billow, config); // additive - never deletes
224
+ ```
225
+
226
+
227
+ ## Pay an outstanding invoice
228
+
229
+ Call from your backend with a secret key after checking that the signed-in customer owns the invoice:
230
+
231
+ ```ts
232
+ const payment = await billow.invoices.pay(invoiceId, {
233
+ returnUrl: "https://your-app.example/billing",
234
+ });
235
+ // Redirect the customer to payment.checkoutUrl.
236
+ ```
237
+
238
+ REST: `POST /v1/invoices/:id/pay` with `{ "returnUrl": "https://your-app.example/billing" }`.
239
+ The response contains `invoiceId`, `chargeId`, `checkoutUrl`, `status: "processing"`, and `expiresAt`.
240
+ The amount comes from the existing open invoice, including tax. An active checkout is reused;
241
+ conflicting or unresolved payments return HTTP 409. Never put a secret key in a browser link.
242
+
243
+ For a customer-scoped portal session, use `portal.invoices.pay(invoiceId)` or
244
+ `POST /portal/invoices/:id/pay`. Billow enforces invoice ownership for that session.
245
+ The hosted portal also offers Pay now on its overview and invoice list.
246
+
247
+ A redirect back to your application does not prove payment. Refresh the invoice/subscription or
248
+ handle the signed `invoice.paid` and subscription events. Confirmed payment settles the same invoice
249
+ and restores a past-due subscription through Billow's normal settlement process.
250
+
251
+ ### Saved card management
252
+
253
+ List, choose a default, or remove a customer's saved cards from your server:
254
+
255
+ ```ts
256
+ const cards = await billow.customers.paymentMethods.list(customerId);
257
+ await billow.customers.paymentMethods.setDefault(customerId, cardId);
258
+ await billow.customers.paymentMethods.remove(customerId, cardId);
259
+ ```
260
+
261
+ A `BillowPortal` client offers the same operations as `portal.paymentMethods.list()`,
262
+ `portal.paymentMethods.setDefault(cardId)`, and `portal.paymentMethods.remove(cardId)`.
263
+ Its session fixes the customer identity. Responses include masked card details,
264
+ `expiryStatus`, `canSetDefault`, `canRemove`, and `removalBlockedReason`; never card tokens.
265
+
266
+ Choosing a default updates renewal cards on active, trialing, and past-due subscriptions.
267
+ A default or subscription renewal card cannot be removed until a replacement is selected.
268
+ Once those subscriptions have ended, every card can be removed, including the default.
269
+ Cards remain valid through their expiry month and are flagged during their last three
270
+ calendar months. Missing expiry data is reported as `unknown`.
271
+
272
+ ## License
273
+
274
+ MIT. See the `LICENSE` file shipped with this package.