meterflow 0.1.2__tar.gz → 0.2.0__tar.gz

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 (22) hide show
  1. {meterflow-0.1.2 → meterflow-0.2.0}/PKG-INFO +76 -10
  2. {meterflow-0.1.2 → meterflow-0.2.0}/README.md +75 -9
  3. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/_version.py +1 -1
  4. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/client.py +6 -0
  5. meterflow-0.2.0/src/meterflow/resources/__init__.py +1 -0
  6. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/resources/credits.py +19 -9
  7. meterflow-0.2.0/src/meterflow/resources/portal.py +52 -0
  8. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/resources/subscriptions.py +19 -9
  9. meterflow-0.2.0/src/meterflow/resources/webhooks.py +75 -0
  10. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/types.py +110 -3
  11. meterflow-0.1.2/src/meterflow/resources/__init__.py +0 -1
  12. {meterflow-0.1.2 → meterflow-0.2.0}/LICENSE +0 -0
  13. {meterflow-0.1.2 → meterflow-0.2.0}/pyproject.toml +0 -0
  14. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/__init__.py +0 -0
  15. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/_http.py +0 -0
  16. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/_retry.py +0 -0
  17. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/errors.py +0 -0
  18. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/py.typed +0 -0
  19. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/resources/entitlements.py +0 -0
  20. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/resources/plans.py +0 -0
  21. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/resources/usage.py +0 -0
  22. {meterflow-0.1.2 → meterflow-0.2.0}/src/meterflow/webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meterflow
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Official Python SDK for MeterFlow — usage-based billing, credit management, and metering.
5
5
  Project-URL: Homepage, https://meter-flow.com
6
6
  Project-URL: Documentation, https://meter-flow.com/guide
@@ -75,6 +75,7 @@ Track what your customers use, enforce credit balances, gate features, and manag
75
75
  - [Usage events](#usage-events)
76
76
  - [Subscriptions](#subscriptions)
77
77
  - [Plans](#plans)
78
+ - [Webhook receipts](#webhook-receipts)
78
79
  - [Entitlements — ask before you act](#entitlements--ask-before-you-act)
79
80
  - [Idempotency — safe retries for writes](#idempotency--safe-retries-for-writes)
80
81
  - [Automatic retries](#automatic-retries)
@@ -235,13 +236,26 @@ bal["total_granted"] # "5250.000000"
235
236
  bal["total_consumed"] # "1705.000000"
236
237
  ```
237
238
 
238
- **Transactions** — the full history, newest first:
239
+ **Transactions** — the history, newest first, **a page at a time**:
239
240
 
240
241
  ```python
241
- for t in client.credits.transactions("customer_123"):
242
+ page = client.credits.transactions("customer_123") # 50 rows by default
243
+ for t in page["data"]:
242
244
  print(t["transaction_type"], t["amount"], t["balance_after"], t["description"], t["created_at"])
245
+
246
+ # The whole history, page by page. `next_cursor` is None once there is nothing after it.
247
+ cursor = None
248
+ while True:
249
+ page = client.credits.transactions("customer_123", limit=200, cursor=cursor)
250
+ handle(page["data"])
251
+ cursor = page["next_cursor"]
252
+ if cursor is None:
253
+ break
243
254
  ```
244
255
 
256
+ > **The cursor is opaque.** Pass back exactly what `next_cursor` gave you; do not build one, and do
257
+ > not read anything out of it. `limit` is 1–200 and defaults to 50.
258
+
245
259
  > **Amounts are decimal strings.** Balances and amounts come back as strings (`"3545.000000"`) to avoid floating-point drift on money-like values. Parse deliberately (`decimal.Decimal(...)`) when you need arithmetic. When *sending* an amount, a number or a numeric string are both accepted.
246
260
 
247
261
  ### Usage events
@@ -254,7 +268,7 @@ Record an event every time a customer does the thing you charge for. Events are
254
268
  client.usage.record({
255
269
  "event_name": "images.generated",
256
270
  "customer_external_id": "customer_123",
257
- "value": 1, # what the meter aggregates: 1 for counts, seconds/MB/etc. for sums
271
+ "value": 1, # optional — what the meter aggregates: 1 for counts, seconds/MB/etc. for sums
258
272
  "properties": {}, # free-form context; {} when unused
259
273
  "timestamp": "2026-09-30T10:00:00Z", # optional — defaults to arrival time on the server
260
274
  })
@@ -312,7 +326,8 @@ This is usually the difference between fitting a plan and outgrowing it: 1,000 u
312
326
 
313
327
  Two things to keep in mind:
314
328
 
315
- - **It only helps a `sum` meter.** A `count` meter ignores `value` and counts occurrences, so there each occurrence has to be its own event. `max`, `min` and `unique_count` read `value` as a measurement, not a quantity to add.
329
+ - **It only helps a `sum` meter.** A `count` meter ignores `value` and counts occurrences, so there each occurrence has to be its own event. `max` and `min` read `value` as a measurement, not a quantity to add, and `unique_count` does not read it at all — it counts distinct values of the property its meter names.
330
+ - **`value` is optional.** Leave it out and MeterFlow fills it in: for a `sum`, `max` or `min` meter that names an `aggregation_field`, the numeric value of that property on the event; otherwise `1`. So the event above can be written with `"properties": {"tokens": 1300}` and no `"value"`, as long as the meter's `aggregation_field` is `tokens`. An explicit `value` always wins, a property that is absent or is not a number falls through to `1`, and a negative one is refused with a `422`.
316
331
  - **Billing is unchanged.** A metered plan limit charges per unit — the event's `value` — so 100 units cost the same whether they arrived as 100 events or one. You are not rounding anyone's bill by batching.
317
332
 
318
333
  **Summary** — a customer's usage, broken down by meter:
@@ -353,8 +368,12 @@ print(sub["status"]) # "trialing" if the plan has trial days, else "active"
353
368
  **List / get:**
354
369
 
355
370
  ```python
356
- everyone = client.subscriptions.list() # whole project (in your key's environment)
371
+ page = client.subscriptions.list() # whole project (in your key's environment)
357
372
  theirs = client.subscriptions.list(customer_id="customer_123") # one customer
373
+ more = client.subscriptions.list(limit=200, cursor=page["next_cursor"])
374
+
375
+ for subscription in page["data"]:
376
+ print(subscription["customer_external_id"], subscription["status"])
358
377
  one = client.subscriptions.get(sub["id"])
359
378
  ```
360
379
 
@@ -391,6 +410,27 @@ plan["features"] # ["sso", "priority-support"] — the on/off keys check(
391
410
 
392
411
  Typical use: render your pricing page or signup flow from `plans.list()` so it can never drift from what billing actually enforces.
393
412
 
413
+ ### Webhook receipts
414
+
415
+ Webhooks are **read-only** through the SDK too: endpoints are registered in the dashboard, where the signing secret is shown once and never again. What the SDK gives you is the other half — which endpoints exist, and what MeterFlow actually sent them.
416
+
417
+ ```python
418
+ endpoints = client.webhooks.list() # this project's endpoints
419
+ receipts = client.webhooks.deliveries(endpoint_id) # newest first
420
+ receipts[0]["event_type"] # "usage.recorded"
421
+ receipts[0]["status"] # "pending" | "retrying" | "success" | "failed"
422
+ receipts[0]["response_code"] # what your server answered — 200, 500, or None if it never answered
423
+ receipts[0]["attempts"] # how many times we have tried
424
+ receipts[0]["next_retry_at"] # when we will try again, or None
425
+ receipts[0]["payload"] # exactly what we sent
426
+ ```
427
+
428
+ Typical use: answering "did that event reach my endpoint?" from your own tooling instead of opening the dashboard — and reading `response_body` when the answer is no.
429
+
430
+ Two things to know. The list is capped at the **most recent 100** attempts per endpoint; it is a debugging surface, not an export. And it follows your key: a `mf_test_*` key reads test receipts, a `mf_live_*` key live ones, while the endpoint list itself is shared by both worlds, because an endpoint is configuration and receives events from each.
431
+
432
+ > This is not the same as [verifying webhooks](#verifying-webhooks) below. That proves a request your server just received really came from us; this reads what we sent.
433
+
394
434
  ### Entitlements — ask before you act
395
435
 
396
436
  The one call to make **before** doing work for a customer: "may they use this feature — and how much is left?" A **hard** limit says no here; recording usage afterwards never blocks, so an app that skips this call gets an honest ledger but no enforcement.
@@ -399,7 +439,7 @@ The one call to make **before** doing work for a customer: "may they use this fe
399
439
  gate = client.entitlements.check("customer_123", "images.generated")
400
440
 
401
441
  if not gate["allowed"]:
402
- # gate["reason"]: "hard_limit_reached" | "no_subscription" | "not_in_plan" | "insufficient_credits"
442
+ # gate["reason"]: "hard_limit_reached" | "no_subscription" | "not_in_plan" | "insufficient_credits" | "spend_cap_reached"
403
443
  return show_upgrade_prompt(gate) # gate["used"] / gate["included"] / gate["period_end"] are there for the copy
404
444
 
405
445
  generate_image()
@@ -424,6 +464,30 @@ result["entitlements"]
424
464
 
425
465
  Prefer to be told rather than to ask? Subscribe a webhook to `limit.reached` (see below).
426
466
 
467
+ ### The balance portal — a page you don't have to build
468
+
469
+ Mint a short-lived link to a MeterFlow-hosted page showing **one** customer's balance and ledger.
470
+ Call it from your server; send the ``url`` to that customer.
471
+
472
+ ```python
473
+ link = client.portal.create_session({
474
+ "customer_external_id": "customer_123",
475
+ "topup_url": "https://acme.test/billing", # your checkout — "buy more" goes here
476
+ "return_url": "https://acme.test/app", # where "done" goes
477
+ "expires_in_minutes": 60, # default 60, maximum 1440
478
+ })
479
+
480
+ send_email(customer.email, f"Your balance: {link['url']}")
481
+ ```
482
+
483
+ The token in that link reads **that one customer's** balance and ledger, in the environment of the
484
+ key you minted it with, and nothing else: it cannot write, cannot name another customer, and is
485
+ refused everywhere else in the API. It is never your API key — never send that to anyone.
486
+
487
+ > **MeterFlow never takes the payment.** `topup_url` is a link we carry and hand back to the page;
488
+ > what happens there is between you and your own payment provider. Grant the credits with
489
+ > `credits.grant()` when your checkout tells you it was paid.
490
+
427
491
  ## Idempotency — safe retries for writes
428
492
 
429
493
  Connections drop. When your app can't tell whether a write arrived, the correct move is to send it again **with the same idempotency key** — MeterFlow recognises the key and acts only once:
@@ -538,10 +602,12 @@ Every delivery is a flat JSON object (`Content-Type: application/json`) with two
538
602
  | `subscription.created` | a subscription is created | `customer_id`, `plan_id`, `subscription_id`, `status` |
539
603
  | `subscription.updated` | a subscription is updated | `customer_id`, `subscription_id`, `status` |
540
604
  | `limit.reached` | a customer first reaches a **hard** or **soft** limit's included units this billing period (once per meter per period; metered limits report through `credit.deducted` instead) | `customer_id`, `subscription_id`, `feature` (the meter's `event_name`), `meter_id`, `limit_type`, `included`, `used`, `period_start`, `period_end` |
541
- | `balance.low` | a deduction takes a customer's balance **down through** this endpoint's `low_balance_threshold` — a crossing, so it fires once and not again while they stay below. Needs the threshold set on the webhook; without one it never fires | `customer_id`, `balance`, `threshold`, `transaction_id` |
605
+ | `balance.low` | a spend takes a customer's balance **down through** this endpoint's `low_balance_threshold` — a crossing, so it fires once and not again while they stay below. Raised by both doors that spend: `credits.deduct` and a metered usage charge. **Both crossing events follow from the endpoint's `low_balance_threshold`: set one and the API adds them, clear it and the API removes them — so a line is the only thing to configure.** | `customer_id`, `balance`, `threshold`, `suggested_topup` (the subscription's `topup_amount`, `null` when unset), `transaction_id` |
542
606
  | `balance.exhausted` | a balance reaches zero, **or** a request is refused for want of credits (a deduction returning 402, or `entitlements.check` answering `insufficient_credits`). Unlike `balance.low` this repeats: every refused request is its own blocked request | `customer_id`, `balance`, `reason` (`balance_zero` / `insufficient_credits`), `requested_amount` and `feature` where they apply, `transaction_id` on the zero case |
543
- | `subscription.renewed` | a billing period rolls forward (the renewer; no `subscription.updated` is emitted for a renewal) | `customer_id`, `subscription_id`, `plan_id`, `current_period_start`, `current_period_end`, `credits_granted` |
607
+ | `subscription.renewed` | a billing period opens — either a paid period rolling forward or a **trial converting** to its first paid period, told apart by `reason` (the renewer emits no `subscription.updated` for either) | `customer_id`, `subscription_id`, `plan_id`, `status`, `reason` (`period_renewed` / `trial_converted`), `current_period_start`, `current_period_end`, `credits_granted` |
544
608
  | `subscription.canceled` | a subscription's status becomes canceled — fired **alongside** `subscription.updated`, once, on the call that actually cancels | `customer_id`, `subscription_id`, `plan_id`, `canceled_at` |
609
+ | `balance.topped_up` | a grant takes a customer's balance **up through** this endpoint's `low_balance_threshold` — the upward mirror of `balance.low`, on the same per-endpoint line, so the endpoint told "low" is the one told "recovered". Subscribed together with `balance.low` by setting the threshold, never on its own | `customer_id`, `balance`, `threshold`, `amount`, `transaction_id` |
610
+ | `cap.reached` | a customer's credit spending reaches their subscription's `spend_cap` for the period — once per subscription per period, whether they landed on the cap or were refused by it. A refusal that still leaves headroom is not a reach and does not fire | `customer_id`, `subscription_id`, `cap`, `spent`, `period_start`, `period_end` |
545
611
 
546
612
  For example, a `credit.granted` delivery:
547
613
 
@@ -605,7 +671,7 @@ with MeterFlow(api_key=os.environ["METERFLOW_API_KEY"]) as client:
605
671
 
606
672
  # Support asks: "what's Ana's situation?"
607
673
  balance = client.credits.balance("ana")
608
- history = client.credits.transactions("ana")
674
+ history = client.credits.transactions("ana")["data"]
609
675
  usage = client.usage.summary("ana")
610
676
  entitlements = client.entitlements.get("ana")
611
677
  ```
@@ -23,6 +23,7 @@ Track what your customers use, enforce credit balances, gate features, and manag
23
23
  - [Usage events](#usage-events)
24
24
  - [Subscriptions](#subscriptions)
25
25
  - [Plans](#plans)
26
+ - [Webhook receipts](#webhook-receipts)
26
27
  - [Entitlements — ask before you act](#entitlements--ask-before-you-act)
27
28
  - [Idempotency — safe retries for writes](#idempotency--safe-retries-for-writes)
28
29
  - [Automatic retries](#automatic-retries)
@@ -183,13 +184,26 @@ bal["total_granted"] # "5250.000000"
183
184
  bal["total_consumed"] # "1705.000000"
184
185
  ```
185
186
 
186
- **Transactions** — the full history, newest first:
187
+ **Transactions** — the history, newest first, **a page at a time**:
187
188
 
188
189
  ```python
189
- for t in client.credits.transactions("customer_123"):
190
+ page = client.credits.transactions("customer_123") # 50 rows by default
191
+ for t in page["data"]:
190
192
  print(t["transaction_type"], t["amount"], t["balance_after"], t["description"], t["created_at"])
193
+
194
+ # The whole history, page by page. `next_cursor` is None once there is nothing after it.
195
+ cursor = None
196
+ while True:
197
+ page = client.credits.transactions("customer_123", limit=200, cursor=cursor)
198
+ handle(page["data"])
199
+ cursor = page["next_cursor"]
200
+ if cursor is None:
201
+ break
191
202
  ```
192
203
 
204
+ > **The cursor is opaque.** Pass back exactly what `next_cursor` gave you; do not build one, and do
205
+ > not read anything out of it. `limit` is 1–200 and defaults to 50.
206
+
193
207
  > **Amounts are decimal strings.** Balances and amounts come back as strings (`"3545.000000"`) to avoid floating-point drift on money-like values. Parse deliberately (`decimal.Decimal(...)`) when you need arithmetic. When *sending* an amount, a number or a numeric string are both accepted.
194
208
 
195
209
  ### Usage events
@@ -202,7 +216,7 @@ Record an event every time a customer does the thing you charge for. Events are
202
216
  client.usage.record({
203
217
  "event_name": "images.generated",
204
218
  "customer_external_id": "customer_123",
205
- "value": 1, # what the meter aggregates: 1 for counts, seconds/MB/etc. for sums
219
+ "value": 1, # optional — what the meter aggregates: 1 for counts, seconds/MB/etc. for sums
206
220
  "properties": {}, # free-form context; {} when unused
207
221
  "timestamp": "2026-09-30T10:00:00Z", # optional — defaults to arrival time on the server
208
222
  })
@@ -260,7 +274,8 @@ This is usually the difference between fitting a plan and outgrowing it: 1,000 u
260
274
 
261
275
  Two things to keep in mind:
262
276
 
263
- - **It only helps a `sum` meter.** A `count` meter ignores `value` and counts occurrences, so there each occurrence has to be its own event. `max`, `min` and `unique_count` read `value` as a measurement, not a quantity to add.
277
+ - **It only helps a `sum` meter.** A `count` meter ignores `value` and counts occurrences, so there each occurrence has to be its own event. `max` and `min` read `value` as a measurement, not a quantity to add, and `unique_count` does not read it at all — it counts distinct values of the property its meter names.
278
+ - **`value` is optional.** Leave it out and MeterFlow fills it in: for a `sum`, `max` or `min` meter that names an `aggregation_field`, the numeric value of that property on the event; otherwise `1`. So the event above can be written with `"properties": {"tokens": 1300}` and no `"value"`, as long as the meter's `aggregation_field` is `tokens`. An explicit `value` always wins, a property that is absent or is not a number falls through to `1`, and a negative one is refused with a `422`.
264
279
  - **Billing is unchanged.** A metered plan limit charges per unit — the event's `value` — so 100 units cost the same whether they arrived as 100 events or one. You are not rounding anyone's bill by batching.
265
280
 
266
281
  **Summary** — a customer's usage, broken down by meter:
@@ -301,8 +316,12 @@ print(sub["status"]) # "trialing" if the plan has trial days, else "active"
301
316
  **List / get:**
302
317
 
303
318
  ```python
304
- everyone = client.subscriptions.list() # whole project (in your key's environment)
319
+ page = client.subscriptions.list() # whole project (in your key's environment)
305
320
  theirs = client.subscriptions.list(customer_id="customer_123") # one customer
321
+ more = client.subscriptions.list(limit=200, cursor=page["next_cursor"])
322
+
323
+ for subscription in page["data"]:
324
+ print(subscription["customer_external_id"], subscription["status"])
306
325
  one = client.subscriptions.get(sub["id"])
307
326
  ```
308
327
 
@@ -339,6 +358,27 @@ plan["features"] # ["sso", "priority-support"] — the on/off keys check(
339
358
 
340
359
  Typical use: render your pricing page or signup flow from `plans.list()` so it can never drift from what billing actually enforces.
341
360
 
361
+ ### Webhook receipts
362
+
363
+ Webhooks are **read-only** through the SDK too: endpoints are registered in the dashboard, where the signing secret is shown once and never again. What the SDK gives you is the other half — which endpoints exist, and what MeterFlow actually sent them.
364
+
365
+ ```python
366
+ endpoints = client.webhooks.list() # this project's endpoints
367
+ receipts = client.webhooks.deliveries(endpoint_id) # newest first
368
+ receipts[0]["event_type"] # "usage.recorded"
369
+ receipts[0]["status"] # "pending" | "retrying" | "success" | "failed"
370
+ receipts[0]["response_code"] # what your server answered — 200, 500, or None if it never answered
371
+ receipts[0]["attempts"] # how many times we have tried
372
+ receipts[0]["next_retry_at"] # when we will try again, or None
373
+ receipts[0]["payload"] # exactly what we sent
374
+ ```
375
+
376
+ Typical use: answering "did that event reach my endpoint?" from your own tooling instead of opening the dashboard — and reading `response_body` when the answer is no.
377
+
378
+ Two things to know. The list is capped at the **most recent 100** attempts per endpoint; it is a debugging surface, not an export. And it follows your key: a `mf_test_*` key reads test receipts, a `mf_live_*` key live ones, while the endpoint list itself is shared by both worlds, because an endpoint is configuration and receives events from each.
379
+
380
+ > This is not the same as [verifying webhooks](#verifying-webhooks) below. That proves a request your server just received really came from us; this reads what we sent.
381
+
342
382
  ### Entitlements — ask before you act
343
383
 
344
384
  The one call to make **before** doing work for a customer: "may they use this feature — and how much is left?" A **hard** limit says no here; recording usage afterwards never blocks, so an app that skips this call gets an honest ledger but no enforcement.
@@ -347,7 +387,7 @@ The one call to make **before** doing work for a customer: "may they use this fe
347
387
  gate = client.entitlements.check("customer_123", "images.generated")
348
388
 
349
389
  if not gate["allowed"]:
350
- # gate["reason"]: "hard_limit_reached" | "no_subscription" | "not_in_plan" | "insufficient_credits"
390
+ # gate["reason"]: "hard_limit_reached" | "no_subscription" | "not_in_plan" | "insufficient_credits" | "spend_cap_reached"
351
391
  return show_upgrade_prompt(gate) # gate["used"] / gate["included"] / gate["period_end"] are there for the copy
352
392
 
353
393
  generate_image()
@@ -372,6 +412,30 @@ result["entitlements"]
372
412
 
373
413
  Prefer to be told rather than to ask? Subscribe a webhook to `limit.reached` (see below).
374
414
 
415
+ ### The balance portal — a page you don't have to build
416
+
417
+ Mint a short-lived link to a MeterFlow-hosted page showing **one** customer's balance and ledger.
418
+ Call it from your server; send the ``url`` to that customer.
419
+
420
+ ```python
421
+ link = client.portal.create_session({
422
+ "customer_external_id": "customer_123",
423
+ "topup_url": "https://acme.test/billing", # your checkout — "buy more" goes here
424
+ "return_url": "https://acme.test/app", # where "done" goes
425
+ "expires_in_minutes": 60, # default 60, maximum 1440
426
+ })
427
+
428
+ send_email(customer.email, f"Your balance: {link['url']}")
429
+ ```
430
+
431
+ The token in that link reads **that one customer's** balance and ledger, in the environment of the
432
+ key you minted it with, and nothing else: it cannot write, cannot name another customer, and is
433
+ refused everywhere else in the API. It is never your API key — never send that to anyone.
434
+
435
+ > **MeterFlow never takes the payment.** `topup_url` is a link we carry and hand back to the page;
436
+ > what happens there is between you and your own payment provider. Grant the credits with
437
+ > `credits.grant()` when your checkout tells you it was paid.
438
+
375
439
  ## Idempotency — safe retries for writes
376
440
 
377
441
  Connections drop. When your app can't tell whether a write arrived, the correct move is to send it again **with the same idempotency key** — MeterFlow recognises the key and acts only once:
@@ -486,10 +550,12 @@ Every delivery is a flat JSON object (`Content-Type: application/json`) with two
486
550
  | `subscription.created` | a subscription is created | `customer_id`, `plan_id`, `subscription_id`, `status` |
487
551
  | `subscription.updated` | a subscription is updated | `customer_id`, `subscription_id`, `status` |
488
552
  | `limit.reached` | a customer first reaches a **hard** or **soft** limit's included units this billing period (once per meter per period; metered limits report through `credit.deducted` instead) | `customer_id`, `subscription_id`, `feature` (the meter's `event_name`), `meter_id`, `limit_type`, `included`, `used`, `period_start`, `period_end` |
489
- | `balance.low` | a deduction takes a customer's balance **down through** this endpoint's `low_balance_threshold` — a crossing, so it fires once and not again while they stay below. Needs the threshold set on the webhook; without one it never fires | `customer_id`, `balance`, `threshold`, `transaction_id` |
553
+ | `balance.low` | a spend takes a customer's balance **down through** this endpoint's `low_balance_threshold` — a crossing, so it fires once and not again while they stay below. Raised by both doors that spend: `credits.deduct` and a metered usage charge. **Both crossing events follow from the endpoint's `low_balance_threshold`: set one and the API adds them, clear it and the API removes them — so a line is the only thing to configure.** | `customer_id`, `balance`, `threshold`, `suggested_topup` (the subscription's `topup_amount`, `null` when unset), `transaction_id` |
490
554
  | `balance.exhausted` | a balance reaches zero, **or** a request is refused for want of credits (a deduction returning 402, or `entitlements.check` answering `insufficient_credits`). Unlike `balance.low` this repeats: every refused request is its own blocked request | `customer_id`, `balance`, `reason` (`balance_zero` / `insufficient_credits`), `requested_amount` and `feature` where they apply, `transaction_id` on the zero case |
491
- | `subscription.renewed` | a billing period rolls forward (the renewer; no `subscription.updated` is emitted for a renewal) | `customer_id`, `subscription_id`, `plan_id`, `current_period_start`, `current_period_end`, `credits_granted` |
555
+ | `subscription.renewed` | a billing period opens — either a paid period rolling forward or a **trial converting** to its first paid period, told apart by `reason` (the renewer emits no `subscription.updated` for either) | `customer_id`, `subscription_id`, `plan_id`, `status`, `reason` (`period_renewed` / `trial_converted`), `current_period_start`, `current_period_end`, `credits_granted` |
492
556
  | `subscription.canceled` | a subscription's status becomes canceled — fired **alongside** `subscription.updated`, once, on the call that actually cancels | `customer_id`, `subscription_id`, `plan_id`, `canceled_at` |
557
+ | `balance.topped_up` | a grant takes a customer's balance **up through** this endpoint's `low_balance_threshold` — the upward mirror of `balance.low`, on the same per-endpoint line, so the endpoint told "low" is the one told "recovered". Subscribed together with `balance.low` by setting the threshold, never on its own | `customer_id`, `balance`, `threshold`, `amount`, `transaction_id` |
558
+ | `cap.reached` | a customer's credit spending reaches their subscription's `spend_cap` for the period — once per subscription per period, whether they landed on the cap or were refused by it. A refusal that still leaves headroom is not a reach and does not fire | `customer_id`, `subscription_id`, `cap`, `spent`, `period_start`, `period_end` |
493
559
 
494
560
  For example, a `credit.granted` delivery:
495
561
 
@@ -553,7 +619,7 @@ with MeterFlow(api_key=os.environ["METERFLOW_API_KEY"]) as client:
553
619
 
554
620
  # Support asks: "what's Ana's situation?"
555
621
  balance = client.credits.balance("ana")
556
- history = client.credits.transactions("ana")
622
+ history = client.credits.transactions("ana")["data"]
557
623
  usage = client.usage.summary("ana")
558
624
  entitlements = client.entitlements.get("ana")
559
625
  ```
@@ -1,3 +1,3 @@
1
1
  # The one place the version lives. pyproject.toml reads it (hatch dynamic version) and the
2
2
  # User-Agent header reports it, so a release bump edits this file and nothing else.
3
- __version__ = "0.1.2"
3
+ __version__ = "0.2.0"
@@ -19,8 +19,10 @@ from ._retry import RetryPolicy
19
19
  from .resources.credits import AsyncCreditsResource, CreditsResource
20
20
  from .resources.entitlements import AsyncEntitlementsResource, EntitlementsResource
21
21
  from .resources.plans import AsyncPlansResource, PlansResource
22
+ from .resources.portal import AsyncPortalResource, PortalResource
22
23
  from .resources.subscriptions import AsyncSubscriptionsResource, SubscriptionsResource
23
24
  from .resources.usage import AsyncUsageResource, UsageResource
25
+ from .resources.webhooks import AsyncWebhooksResource, WebhooksResource
24
26
 
25
27
  DEFAULT_BASE_URL = "https://api.meter-flow.com/api/v1"
26
28
  DEFAULT_TIMEOUT_SECONDS = 30.0
@@ -76,7 +78,9 @@ class MeterFlow(_BaseClient):
76
78
  self.usage = UsageResource(self)
77
79
  self.subscriptions = SubscriptionsResource(self)
78
80
  self.plans = PlansResource(self)
81
+ self.portal = PortalResource(self)
79
82
  self.entitlements = EntitlementsResource(self)
83
+ self.webhooks = WebhooksResource(self)
80
84
 
81
85
  def request(self, spec: RequestSpec) -> Any:
82
86
  """Internal — used by resource classes; not part of the public API."""
@@ -121,7 +125,9 @@ class AsyncMeterFlow(_BaseClient):
121
125
  self.usage = AsyncUsageResource(self)
122
126
  self.subscriptions = AsyncSubscriptionsResource(self)
123
127
  self.plans = AsyncPlansResource(self)
128
+ self.portal = AsyncPortalResource(self)
124
129
  self.entitlements = AsyncEntitlementsResource(self)
130
+ self.webhooks = AsyncWebhooksResource(self)
125
131
 
126
132
  async def request(self, spec: RequestSpec) -> Any:
127
133
  """Internal — used by resource classes; not part of the public API."""
@@ -0,0 +1 @@
1
+ """The six SDK-facing resources, each in a sync and an async flavour sharing one set of request specs."""
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from .._http import RequestSpec, encode_path_segment
8
- from ..types import CreditBalanceResponse, CreditDeductRequest, CreditGrantRequest, CreditTransactionResponse
8
+ from ..types import CreditBalanceResponse, CreditDeductRequest, CreditGrantRequest, CreditTransactionPage, CreditTransactionResponse
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from ..client import AsyncMeterFlow, MeterFlow
@@ -23,8 +23,8 @@ def balance_spec(customer_id: str) -> RequestSpec:
23
23
  return RequestSpec("GET", f"credits/{encode_path_segment(customer_id)}/balance")
24
24
 
25
25
 
26
- def transactions_spec(customer_id: str) -> RequestSpec:
27
- return RequestSpec("GET", f"credits/{encode_path_segment(customer_id)}/transactions")
26
+ def transactions_spec(customer_id: str, limit: int | None, cursor: str | None) -> RequestSpec:
27
+ return RequestSpec("GET", f"credits/{encode_path_segment(customer_id)}/transactions", query={"limit": limit, "cursor": cursor})
28
28
 
29
29
 
30
30
  class CreditsResource:
@@ -46,9 +46,14 @@ class CreditsResource:
46
46
  result: CreditBalanceResponse = self._client.request(balance_spec(customer_id))
47
47
  return result
48
48
 
49
- def transactions(self, customer_id: str) -> list[CreditTransactionResponse]:
50
- """The full history, newest first."""
51
- result: list[CreditTransactionResponse] = self._client.request(transactions_spec(customer_id))
49
+ def transactions(self, customer_id: str, *, limit: int | None = None, cursor: str | None = None) -> CreditTransactionPage:
50
+ """One page of the ledger, newest first.
51
+
52
+ The ledger grows with every charge, so it is read a page at a time: ``data`` holds the rows
53
+ and ``next_cursor`` the mark for the page after this one (``None`` on the last). Pass it
54
+ back as ``cursor`` unchanged — it is opaque, and nothing but this API reads it.
55
+ """
56
+ result: CreditTransactionPage = self._client.request(transactions_spec(customer_id, limit, cursor))
52
57
  return result
53
58
 
54
59
 
@@ -71,7 +76,12 @@ class AsyncCreditsResource:
71
76
  result: CreditBalanceResponse = await self._client.request(balance_spec(customer_id))
72
77
  return result
73
78
 
74
- async def transactions(self, customer_id: str) -> list[CreditTransactionResponse]:
75
- """The full history, newest first."""
76
- result: list[CreditTransactionResponse] = await self._client.request(transactions_spec(customer_id))
79
+ async def transactions(self, customer_id: str, *, limit: int | None = None, cursor: str | None = None) -> CreditTransactionPage:
80
+ """One page of the ledger, newest first.
81
+
82
+ The ledger grows with every charge, so it is read a page at a time: ``data`` holds the rows
83
+ and ``next_cursor`` the mark for the page after this one (``None`` on the last). Pass it
84
+ back as ``cursor`` unchanged — it is opaque, and nothing but this API reads it.
85
+ """
86
+ result: CreditTransactionPage = await self._client.request(transactions_spec(customer_id, limit, cursor))
77
87
  return result
@@ -0,0 +1,52 @@
1
+ """The hosted balance portal: mint a link for one of your customers.
2
+
3
+ Only the mint is here. The two reads behind the page — the balance and the ledger — are made by
4
+ the person holding the link, with the link's own token; this client holds a secret key, and the
5
+ two must never be the same credential.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from .._http import RequestSpec
13
+ from ..types import PortalLinkRequest, PortalLinkResponse
14
+
15
+ if TYPE_CHECKING:
16
+ from ..client import AsyncMeterFlow, MeterFlow
17
+
18
+ _DOC = """Mint a link to the hosted balance page for one of your customers.
19
+
20
+ Call this from **your** server and send the ``url`` to that customer. It carries a
21
+ short-lived token that reads their balance and their ledger and nothing else, in the
22
+ environment of the key you called with; it is never your API key and it can write nothing.
23
+
24
+ The page shows the balance and sends "buy more" to your own checkout (``topup_url``):
25
+ MeterFlow never takes the payment. ``expires_in_minutes`` defaults to 60, maximum 1440.
26
+ """
27
+
28
+
29
+ def create_session_spec(body: PortalLinkRequest) -> RequestSpec:
30
+ return RequestSpec("POST", "portal/sessions", body=body)
31
+
32
+
33
+ class PortalResource:
34
+ def __init__(self, client: MeterFlow) -> None:
35
+ self._client = client
36
+
37
+ def create_session(self, body: PortalLinkRequest) -> PortalLinkResponse:
38
+ result: PortalLinkResponse = self._client.request(create_session_spec(body))
39
+ return result
40
+
41
+ create_session.__doc__ = _DOC
42
+
43
+
44
+ class AsyncPortalResource:
45
+ def __init__(self, client: AsyncMeterFlow) -> None:
46
+ self._client = client
47
+
48
+ async def create_session(self, body: PortalLinkRequest) -> PortalLinkResponse:
49
+ result: PortalLinkResponse = await self._client.request(create_session_spec(body))
50
+ return result
51
+
52
+ create_session.__doc__ = _DOC
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from .._http import RequestSpec, encode_path_segment
8
- from ..types import SubscriptionCreateRequest, SubscriptionResponse, SubscriptionUpdateRequest
8
+ from ..types import SubscriptionCreateRequest, SubscriptionPage, SubscriptionResponse, SubscriptionUpdateRequest
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from ..client import AsyncMeterFlow, MeterFlow
@@ -15,8 +15,8 @@ def create_spec(body: SubscriptionCreateRequest, idempotency_key: str | None) ->
15
15
  return RequestSpec("POST", "subscriptions", body=body, idempotency_key=idempotency_key)
16
16
 
17
17
 
18
- def list_spec(customer_id: str | None) -> RequestSpec:
19
- return RequestSpec("GET", "subscriptions", query={"customer_id": customer_id})
18
+ def list_spec(customer_id: str | None, limit: int | None, cursor: str | None) -> RequestSpec:
19
+ return RequestSpec("GET", "subscriptions", query={"customer_id": customer_id, "limit": limit, "cursor": cursor})
20
20
 
21
21
 
22
22
  def get_spec(subscription_id: str) -> RequestSpec:
@@ -40,9 +40,14 @@ class SubscriptionsResource:
40
40
  result: SubscriptionResponse = self._client.request(create_spec(body, idempotency_key))
41
41
  return result
42
42
 
43
- def list(self, *, customer_id: str | None = None) -> list[SubscriptionResponse]:
44
- """The project's subscriptions in this key's environment, optionally one customer's."""
45
- result: list[SubscriptionResponse] = self._client.request(list_spec(customer_id))
43
+ def list(self, *, customer_id: str | None = None, limit: int | None = None, cursor: str | None = None) -> SubscriptionPage:
44
+ """One page of the project's subscriptions in this key's environment, newest first.
45
+
46
+ ``data`` holds the rows and ``next_cursor`` the mark for the page after this one (``None``
47
+ on the last). Pass it back as ``cursor`` unchanged — it is opaque, and nothing but this API
48
+ reads it. ``customer_id`` narrows the list to one customer.
49
+ """
50
+ result: SubscriptionPage = self._client.request(list_spec(customer_id, limit, cursor))
46
51
  return result
47
52
 
48
53
  def get(self, subscription_id: str) -> SubscriptionResponse:
@@ -68,9 +73,14 @@ class AsyncSubscriptionsResource:
68
73
  result: SubscriptionResponse = await self._client.request(create_spec(body, idempotency_key))
69
74
  return result
70
75
 
71
- async def list(self, *, customer_id: str | None = None) -> list[SubscriptionResponse]:
72
- """The project's subscriptions in this key's environment, optionally one customer's."""
73
- result: list[SubscriptionResponse] = await self._client.request(list_spec(customer_id))
76
+ async def list(self, *, customer_id: str | None = None, limit: int | None = None, cursor: str | None = None) -> SubscriptionPage:
77
+ """One page of the project's subscriptions in this key's environment, newest first.
78
+
79
+ ``data`` holds the rows and ``next_cursor`` the mark for the page after this one (``None``
80
+ on the last). Pass it back as ``cursor`` unchanged — it is opaque, and nothing but this API
81
+ reads it. ``customer_id`` narrows the list to one customer.
82
+ """
83
+ result: SubscriptionPage = await self._client.request(list_spec(customer_id, limit, cursor))
74
84
  return result
75
85
 
76
86
  async def get(self, subscription_id: str) -> SubscriptionResponse:
@@ -0,0 +1,75 @@
1
+ """Webhook receipts — read-only through the SDK: endpoints are registered in the dashboard.
2
+
3
+ An endpoint is created, edited and deleted where the signing secret can be shown once and never
4
+ again. What an SDK consumer needs at 3am is the other half: which endpoints exist, and what
5
+ MeterFlow actually sent them. That is this module.
6
+
7
+ Not to be confused with :mod:`meterflow.webhook`, which verifies the signature on a webhook your
8
+ server has just received. This reads what we sent; that proves we sent it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from .._http import RequestSpec, encode_path_segment
16
+ from ..types import WebhookDeliveryResponse, WebhookResponse
17
+
18
+ if TYPE_CHECKING:
19
+ from ..client import AsyncMeterFlow, MeterFlow
20
+
21
+ # Spelled as module-level aliases, where `list` is still the builtin. Inside the class bodies below
22
+ # it is not: a method called `list` shadows it for every annotation that follows, so writing
23
+ # `list[WebhookDeliveryResponse]` on `deliveries` resolves to the method and mypy rejects it.
24
+ # `plans.py` never meets this only because its second method returns a bare type.
25
+ WebhookList = list[WebhookResponse]
26
+ DeliveryList = list[WebhookDeliveryResponse]
27
+
28
+
29
+ def list_spec() -> RequestSpec:
30
+ # No project_id: the key's own project is the scope, and a query could not widen it.
31
+ return RequestSpec("GET", "webhooks")
32
+
33
+
34
+ def deliveries_spec(webhook_id: str) -> RequestSpec:
35
+ return RequestSpec("GET", f"webhooks/{encode_path_segment(webhook_id)}/deliveries")
36
+
37
+
38
+ class WebhooksResource:
39
+ def __init__(self, client: MeterFlow) -> None:
40
+ self._client = client
41
+
42
+ def list(self) -> WebhookList:
43
+ """The endpoints registered for this key's project. Never includes a secret."""
44
+ result: WebhookList = self._client.request(list_spec())
45
+ return result
46
+
47
+ def deliveries(self, webhook_id: str) -> DeliveryList:
48
+ """One endpoint's delivery attempts, newest first: what was sent, the HTTP status that came
49
+ back, how many attempts and when the next retry is due.
50
+
51
+ Capped at the most recent 100, and scoped to the key's own environment — a ``mf_test_*``
52
+ key reads test receipts, a ``mf_live_*`` key live ones. Another project's webhook is a 404.
53
+ """
54
+ result: DeliveryList = self._client.request(deliveries_spec(webhook_id))
55
+ return result
56
+
57
+
58
+ class AsyncWebhooksResource:
59
+ def __init__(self, client: AsyncMeterFlow) -> None:
60
+ self._client = client
61
+
62
+ async def list(self) -> WebhookList:
63
+ """The endpoints registered for this key's project. Never includes a secret."""
64
+ result: WebhookList = await self._client.request(list_spec())
65
+ return result
66
+
67
+ async def deliveries(self, webhook_id: str) -> DeliveryList:
68
+ """One endpoint's delivery attempts, newest first: what was sent, the HTTP status that came
69
+ back, how many attempts and when the next retry is due.
70
+
71
+ Capped at the most recent 100, and scoped to the key's own environment — a ``mf_test_*``
72
+ key reads test receipts, a ``mf_live_*`` key live ones. Another project's webhook is a 404.
73
+ """
74
+ result: DeliveryList = await self._client.request(deliveries_spec(webhook_id))
75
+ return result
@@ -6,7 +6,14 @@ from __future__ import annotations
6
6
 
7
7
  from typing import Any, Literal, TypedDict
8
8
 
9
- EntitlementReason = Literal["no_subscription", "not_in_plan", "hard_limit_reached", "soft_limit_exceeded", "insufficient_credits"]
9
+ EntitlementReason = Literal[
10
+ "no_subscription",
11
+ "not_in_plan",
12
+ "hard_limit_reached",
13
+ "soft_limit_exceeded",
14
+ "insufficient_credits",
15
+ "spend_cap_reached",
16
+ ]
10
17
 
11
18
  FeatureType = Literal["boolean", "metered"]
12
19
 
@@ -48,6 +55,19 @@ class CreditGrantRequest(_CreditGrantRequestRequired, total=False):
48
55
  metadata: dict[str, Any]
49
56
 
50
57
 
58
+ class _CreditTransactionPageRequired(TypedDict):
59
+ data: list[CreditTransactionResponse]
60
+
61
+
62
+ class CreditTransactionPage(_CreditTransactionPageRequired, total=False):
63
+ """One page of a customer's credit ledger, newest first. `next_cursor` is the opaque mark for the page after this one, and `null` on the
64
+ last page — pass it back as `cursor` unchanged. The list grows with a customer's traffic, which is why it is paged at all
65
+ (docs/DECISIONS.md, 2026-10-14).
66
+ """
67
+
68
+ next_cursor: str | None
69
+
70
+
51
71
  class CreditTransactionResponse(TypedDict):
52
72
  id: str
53
73
  project_id: str
@@ -108,7 +128,6 @@ class PlanResponse(TypedDict):
108
128
  currency: str
109
129
  trial_days: int
110
130
  is_active: bool
111
- is_public: bool
112
131
  is_published: bool
113
132
  metadata_: dict[str, Any]
114
133
  meter_limits: list[PlanMeterLimitResponse]
@@ -117,16 +136,61 @@ class PlanResponse(TypedDict):
117
136
  updated_at: str
118
137
 
119
138
 
139
+ class _PortalLinkRequestRequired(TypedDict):
140
+ customer_external_id: str
141
+
142
+
143
+ class PortalLinkRequest(_PortalLinkRequestRequired, total=False):
144
+ """One link, for one of the caller's customers. The environment is **not** here: it comes from the API key's prefix, like every other
145
+ SDK-facing write.
146
+ """
147
+
148
+ return_url: str | None
149
+ topup_url: str | None
150
+ expires_in_minutes: int | None
151
+
152
+
153
+ class _PortalLinkResponseRequired(TypedDict):
154
+ url: str
155
+ token: str
156
+ expires_at: str
157
+ customer_external_id: str
158
+ environment: str
159
+
160
+
161
+ class PortalLinkResponse(_PortalLinkResponseRequired, total=False):
162
+ """The link to send, and what it is for. The signing material is never part of this."""
163
+
164
+ return_url: str | None
165
+ topup_url: str | None
166
+
167
+
120
168
  class _SubscriptionCreateRequestRequired(TypedDict):
121
169
  plan_id: str
122
170
  customer_external_id: str
123
171
 
124
172
 
125
173
  class SubscriptionCreateRequest(_SubscriptionCreateRequestRequired, total=False):
174
+ spend_cap: float | str | None
175
+ topup_amount: float | str | None
176
+ max_topups_per_window: int | None
126
177
  metadata: dict[str, Any]
127
178
  idempotency_key: str | None
128
179
 
129
180
 
181
+ class _SubscriptionPageRequired(TypedDict):
182
+ data: list[SubscriptionResponse]
183
+
184
+
185
+ class SubscriptionPage(_SubscriptionPageRequired, total=False):
186
+ """One page of a project's subscriptions, newest first. `next_cursor` is the opaque mark for the page after this one, and `null` on the
187
+ last page — pass it back as `cursor` unchanged. The list grows with a customer's traffic, which is why it is paged at all
188
+ (docs/DECISIONS.md, 2026-10-14).
189
+ """
190
+
191
+ next_cursor: str | None
192
+
193
+
130
194
  class SubscriptionResponse(TypedDict):
131
195
  id: str
132
196
  project_id: str
@@ -137,6 +201,10 @@ class SubscriptionResponse(TypedDict):
137
201
  current_period_start: str
138
202
  current_period_end: str | None
139
203
  trial_end_date: str | None
204
+ spend_cap: str | None
205
+ topup_amount: str | None
206
+ max_topups_per_window: int | None
207
+ idempotency_key: str | None
140
208
  canceled_at: str | None
141
209
  metadata_: dict[str, Any]
142
210
  created_at: str
@@ -145,6 +213,9 @@ class SubscriptionResponse(TypedDict):
145
213
 
146
214
  class SubscriptionUpdateRequest(TypedDict, total=False):
147
215
  status: SubscriptionStatus | None
216
+ spend_cap: float | str | None
217
+ topup_amount: float | str | None
218
+ max_topups_per_window: int | None
148
219
  metadata: dict[str, Any] | None
149
220
 
150
221
 
@@ -158,7 +229,7 @@ class _UsageEventRequestRequired(TypedDict):
158
229
 
159
230
 
160
231
  class UsageEventRequest(_UsageEventRequestRequired, total=False):
161
- value: float | str
232
+ value: float | str | None
162
233
  properties: dict[str, Any]
163
234
  idempotency_key: str | None
164
235
  timestamp: str | None
@@ -206,10 +277,41 @@ class UsageSummaryResponse(TypedDict):
206
277
  meters: list[UsageSummaryItem]
207
278
 
208
279
 
280
+ class WebhookDeliveryResponse(TypedDict):
281
+ id: str
282
+ webhook_id: str
283
+ event_type: str
284
+ payload: dict[str, Any]
285
+ status: str
286
+ response_code: int | None
287
+ response_body: str | None
288
+ attempts: int
289
+ next_retry_at: str | None
290
+ created_at: str
291
+ completed_at: str | None
292
+
293
+
294
+ class WebhookResponse(TypedDict):
295
+ """Returned on all reads — secret is never included."""
296
+
297
+ id: str
298
+ project_id: str
299
+ url: str
300
+ events: list[str]
301
+ low_balance_threshold: str | None
302
+ is_active: bool
303
+ last_triggered_at: str | None
304
+ failure_count: int
305
+ metadata_: dict[str, Any]
306
+ created_at: str
307
+ updated_at: str
308
+
309
+
209
310
  __all__ = [
210
311
  "CreditBalanceResponse",
211
312
  "CreditDeductRequest",
212
313
  "CreditGrantRequest",
314
+ "CreditTransactionPage",
213
315
  "CreditTransactionResponse",
214
316
  "EntitlementReason",
215
317
  "EntitlementResponse",
@@ -217,7 +319,10 @@ __all__ = [
217
319
  "FeatureType",
218
320
  "PlanMeterLimitResponse",
219
321
  "PlanResponse",
322
+ "PortalLinkRequest",
323
+ "PortalLinkResponse",
220
324
  "SubscriptionCreateRequest",
325
+ "SubscriptionPage",
221
326
  "SubscriptionResponse",
222
327
  "SubscriptionStatus",
223
328
  "SubscriptionUpdateRequest",
@@ -226,4 +331,6 @@ __all__ = [
226
331
  "UsageEventResponse",
227
332
  "UsageSummaryItem",
228
333
  "UsageSummaryResponse",
334
+ "WebhookDeliveryResponse",
335
+ "WebhookResponse",
229
336
  ]
@@ -1 +0,0 @@
1
- """The five SDK-facing resources, each in a sync and an async flavour sharing one set of request specs."""
File without changes
File without changes