meterflow 0.3.0 → 0.4.1
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 +45 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
Official Node.js SDK for [MeterFlow](https://
|
|
7
|
+
Official Node.js SDK for [MeterFlow](https://meter-flow.com) — usage-based billing, credit management, and metering.
|
|
8
8
|
|
|
9
9
|
Track what your customers use, enforce credit balances, and manage subscriptions with a few lines of code. TypeScript-first, zero runtime dependencies, built on native `fetch`.
|
|
10
10
|
|
|
@@ -36,7 +36,7 @@ Track what your customers use, enforce credit balances, and manage subscriptions
|
|
|
36
36
|
## Requirements
|
|
37
37
|
|
|
38
38
|
- **Node.js ≥ 20** (the SDK uses the built-in `fetch` — no HTTP library is installed)
|
|
39
|
-
- An API key from your [MeterFlow dashboard](https://
|
|
39
|
+
- An API key from your [MeterFlow dashboard](https://meter-flow.com) (Project → API Keys)
|
|
40
40
|
|
|
41
41
|
> Need Node 18? It reached end-of-life in April 2025 — please upgrade. The last SDK line supporting it is `meterflow@0.2.x`.
|
|
42
42
|
|
|
@@ -87,9 +87,11 @@ Every request authenticates with the API key you pass to the constructor:
|
|
|
87
87
|
| `mf_live_…` | Live | Real customers, real balances |
|
|
88
88
|
| `mf_test_…` | Test | Development, CI, experiments |
|
|
89
89
|
|
|
90
|
+
The key's environment is a **real data boundary, not a label**: inside the same project, `mf_test_` keys read and write a fully separate dataset from `mf_live_` keys — subscriptions, credits, and usage events created with a test key are invisible to live keys (and vice versa), while your meters and plans are shared, so tests always run against your real billing configuration. The same customer id can hold an independent balance and subscription in each environment, and idempotency keys are namespaced per environment. Point your staging/CI at a test key and production at a live key — same project, zero risk of cross-contamination.
|
|
91
|
+
|
|
90
92
|
Keys are created in the dashboard (Project → API Keys) and **shown once** at creation — MeterFlow stores only a fingerprint. If a key leaks, revoke it in the dashboard and mint a new one; revocation is immediate.
|
|
91
93
|
|
|
92
|
-
A key belongs to **one project** and can only see that project's data.
|
|
94
|
+
A key belongs to **one project** and can only see that project's data.
|
|
93
95
|
|
|
94
96
|
```typescript
|
|
95
97
|
const client = new MeterFlow({ apiKey: "mf_test_..." }); // throws immediately if the prefix is neither mf_live_ nor mf_test_
|
|
@@ -115,14 +117,14 @@ Meters and plans are defined in the dashboard. Your app, through this SDK, does
|
|
|
115
117
|
```typescript
|
|
116
118
|
const client = new MeterFlow({
|
|
117
119
|
apiKey: "mf_live_...", // required — mf_live_* or mf_test_*
|
|
118
|
-
baseUrl: "https://api.
|
|
120
|
+
baseUrl: "https://api.meter-flow.com/api/v1", // optional — override for self-hosted / local dev
|
|
119
121
|
timeout: 30_000, // optional — per-request timeout in ms (default 30 s)
|
|
120
122
|
retries: 3, // optional — automatic retries (default 3, 0 disables)
|
|
121
123
|
fetch: customFetch, // optional — inject your own fetch (tests, proxies)
|
|
122
124
|
});
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
`baseUrl` is for pointing at a **different MeterFlow server** — a self-hosted deployment, or a locally running stack if you develop MeterFlow itself (`baseUrl: "http://localhost:8000/api/v1"`). If you use the hosted service, leave it at the default; to test your integration safely, use an `mf_test_` key instead (see [Authentication](#authentication)) — no URL change needed.
|
|
126
128
|
|
|
127
129
|
All configuration lives on the client instance — there is no global state, so you can create multiple clients (e.g. one per project) in the same process.
|
|
128
130
|
|
|
@@ -248,11 +250,13 @@ console.log(sub.status); // "trialing" if the plan has trial days, else "active"
|
|
|
248
250
|
**List / get:**
|
|
249
251
|
|
|
250
252
|
```typescript
|
|
251
|
-
const all = await client.subscriptions.list(); // whole project
|
|
253
|
+
const all = await client.subscriptions.list(); // whole project (in your key's environment)
|
|
252
254
|
const theirs = await client.subscriptions.list({ customer_id: "customer_123" }); // one customer
|
|
253
255
|
const one = await client.subscriptions.get(sub.id);
|
|
254
256
|
```
|
|
255
257
|
|
|
258
|
+
> Like all reads, these are scoped to the key's environment: a live key lists live subscriptions only, a test key test ones only.
|
|
259
|
+
|
|
256
260
|
**Update** — change status or metadata. Statuses: `active`, `trialing`, `past_due`, `paused`, `canceled`, `expired`:
|
|
257
261
|
|
|
258
262
|
```typescript
|
|
@@ -260,12 +264,15 @@ await client.subscriptions.update(sub.id, { status: "paused" });
|
|
|
260
264
|
await client.subscriptions.update(sub.id, { status: "active" }); // reactivate
|
|
261
265
|
```
|
|
262
266
|
|
|
263
|
-
**Cancel
|
|
267
|
+
**Cancel vs delete** — two different operations:
|
|
264
268
|
|
|
265
269
|
```typescript
|
|
266
|
-
await client.subscriptions.
|
|
270
|
+
await client.subscriptions.update(sub.id, { status: "canceled" }); // cancel: the record stays for history
|
|
271
|
+
await client.subscriptions.delete(sub.id); // delete: removes the subscription record entirely
|
|
267
272
|
```
|
|
268
273
|
|
|
274
|
+
Prefer cancelling: it preserves the subscription's history (a canceled subscription can't be reactivated). Reach for `delete` only when you truly want the record gone — e.g. cleaning up test data.
|
|
275
|
+
|
|
269
276
|
### Plans
|
|
270
277
|
|
|
271
278
|
Plans are **read-only** through the SDK — pricing is managed by humans in the dashboard, so a leaked API key can never rewrite your prices.
|
|
@@ -347,7 +354,7 @@ try {
|
|
|
347
354
|
|
|
348
355
|
## Verifying webhooks
|
|
349
356
|
|
|
350
|
-
MeterFlow notifies your app of
|
|
357
|
+
MeterFlow notifies your app of activity you'd otherwise poll for — credits granted or deducted, usage recorded, subscriptions created or updated. Every delivery is signed with **HMAC-SHA256** in the `X-MeterFlow-Signature` header, using the webhook's secret — verify before trusting:
|
|
351
358
|
|
|
352
359
|
```typescript
|
|
353
360
|
import express from "express";
|
|
@@ -375,13 +382,40 @@ Details that matter:
|
|
|
375
382
|
- **Verify the raw bytes.** If you `JSON.parse` first and re-stringify, key ordering/whitespace changes and the signature won't match. `express.json()` users: configure it with a `verify` callback to keep `rawBody`, or use `express.raw` on the webhook route as above.
|
|
376
383
|
- The comparison is **timing-safe** (`crypto.timingSafeEqual`) and returns `false` on any mismatch — it never throws on bad input.
|
|
377
384
|
|
|
385
|
+
### Payload shape & event types
|
|
386
|
+
|
|
387
|
+
Every delivery is a flat JSON object (`Content-Type: application/json`) with two envelope fields — `event` (the type) and `environment` (`"live"` or `"test"`, matching the mode of the API key that caused the activity) — plus type-specific fields:
|
|
388
|
+
|
|
389
|
+
| `event` | Fired when | Extra fields |
|
|
390
|
+
|---|---|---|
|
|
391
|
+
| `usage.recorded` | a usage event is ingested (single or batch) | `customer_id`, `event_name`, `value`, `event_id` |
|
|
392
|
+
| `credit.granted` | credits are granted | `customer_id`, `amount`, `balance_after`, `transaction_id` |
|
|
393
|
+
| `credit.deducted` | credits are deducted | `customer_id`, `amount`, `balance_after`, `transaction_id` |
|
|
394
|
+
| `subscription.created` | a subscription is created | `customer_id`, `plan_id`, `subscription_id`, `status` |
|
|
395
|
+
| `subscription.updated` | a subscription is updated | `customer_id`, `subscription_id`, `status` |
|
|
396
|
+
|
|
397
|
+
For example, a `credit.granted` delivery:
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"event": "credit.granted",
|
|
402
|
+
"environment": "live",
|
|
403
|
+
"customer_id": "cust_123",
|
|
404
|
+
"amount": 500.0,
|
|
405
|
+
"balance_after": 1250.0,
|
|
406
|
+
"transaction_id": "9b2f6c1e-…"
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
When registering a webhook you pick which of these event types it should receive. Deliveries are retried with backoff on non-2xx responses — respond `200` quickly and do the heavy work asynchronously.
|
|
411
|
+
|
|
378
412
|
Webhooks are registered per-project in the dashboard, which is also where you'll find the secret and each delivery attempt's status.
|
|
379
413
|
|
|
380
414
|
## Using the SDK in a browser
|
|
381
415
|
|
|
382
416
|
The root entry point is browser-safe: no Node built-ins, and `fetch` is bound correctly for browser environments. Two rules:
|
|
383
417
|
|
|
384
|
-
1. **Never ship an `mf_live_` key to a browser.** Anyone can read it in DevTools. Browser usage is for trusted, short-lived contexts (internal tools, dashboards) with `mf_test_` or ephemeral keys — your product's customers should always go through **your** backend, which holds the key.
|
|
418
|
+
1. **Never ship an `mf_live_` key to a browser.** Anyone can read it in DevTools. Browser usage is for trusted, short-lived contexts (internal tools, dashboards) with `mf_test_` or ephemeral keys — your product's customers should always go through **your** backend, which holds the key. (A leaked `mf_test_` key is contained by design: it can only ever touch the sandboxed test dataset, never live balances.)
|
|
385
419
|
2. **Don't import `meterflow/webhook` in browser code** — it needs Node's `crypto` (and verifying webhooks in a browser makes no sense anyway: the secret must stay server-side).
|
|
386
420
|
|
|
387
421
|
## TypeScript notes
|
|
@@ -394,6 +428,7 @@ The root entry point is browser-safe: no Node built-ins, and `fetch` is bound co
|
|
|
394
428
|
## Versioning & support
|
|
395
429
|
|
|
396
430
|
- Semantic versioning on the `0.x` line: breaking changes bump the minor, fixes bump the patch.
|
|
431
|
+
- `0.4.0` changed the default API base URL to **`https://api.meter-flow.com/api/v1`** (the platform's domain). If you set `baseUrl` explicitly, nothing changes for you.
|
|
397
432
|
- `0.3.0` raised the Node floor to **≥ 20** (Node 18 is end-of-life). `0.2.x` remains available for Node 18.
|
|
398
433
|
- Tested in CI on Node **20, 22 and 24**.
|
|
399
434
|
- Issues and source: [github.com/meterflowio/meterflow-node](https://github.com/meterflowio/meterflow-node). Include the `requestId` from any `MeterFlowError` when reporting API issues.
|
package/dist/index.cjs
CHANGED
|
@@ -308,7 +308,7 @@ var MeterFlow = class {
|
|
|
308
308
|
}
|
|
309
309
|
this.options = {
|
|
310
310
|
apiKey: options.apiKey,
|
|
311
|
-
baseUrl: options.baseUrl ?? "https://api.
|
|
311
|
+
baseUrl: options.baseUrl ?? "https://api.meter-flow.com/api/v1",
|
|
312
312
|
timeout: options.timeout ?? 3e4,
|
|
313
313
|
retries: options.retries ?? 3,
|
|
314
314
|
// Bind to globalThis: browsers throw "Illegal invocation" if window.fetch is
|
package/dist/index.js
CHANGED
|
@@ -308,7 +308,7 @@ var MeterFlow = class {
|
|
|
308
308
|
}
|
|
309
309
|
this.options = {
|
|
310
310
|
apiKey: options.apiKey,
|
|
311
|
-
baseUrl: options.baseUrl ?? "https://api.
|
|
311
|
+
baseUrl: options.baseUrl ?? "https://api.meter-flow.com/api/v1",
|
|
312
312
|
timeout: options.timeout ?? 3e4,
|
|
313
313
|
retries: options.retries ?? 3,
|
|
314
314
|
// Bind to globalThis: browsers throw "Illegal invocation" if window.fetch is
|
package/dist/index.mjs
CHANGED
|
@@ -274,7 +274,7 @@ var MeterFlow = class {
|
|
|
274
274
|
}
|
|
275
275
|
this.options = {
|
|
276
276
|
apiKey: options.apiKey,
|
|
277
|
-
baseUrl: options.baseUrl ?? "https://api.
|
|
277
|
+
baseUrl: options.baseUrl ?? "https://api.meter-flow.com/api/v1",
|
|
278
278
|
timeout: options.timeout ?? 3e4,
|
|
279
279
|
retries: options.retries ?? 3,
|
|
280
280
|
// Bind to globalThis: browsers throw "Illegal invocation" if window.fetch is
|