meterbase 0.5.0__tar.gz → 0.6.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 (47) hide show
  1. {meterbase-0.5.0 → meterbase-0.6.0}/PKG-INFO +206 -80
  2. {meterbase-0.5.0 → meterbase-0.6.0}/README.md +205 -79
  3. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/_transport.py +6 -5
  4. meterbase-0.6.0/src/meterbase/_async/resources/billing.py +160 -0
  5. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_http.py +1 -1
  6. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/_transport.py +6 -5
  7. meterbase-0.6.0/src/meterbase/_sync/resources/billing.py +164 -0
  8. meterbase-0.6.0/src/meterbase/_version.py +1 -0
  9. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/types.py +110 -7
  10. meterbase-0.5.0/src/meterbase/_async/resources/billing.py +0 -73
  11. meterbase-0.5.0/src/meterbase/_sync/resources/billing.py +0 -77
  12. meterbase-0.5.0/src/meterbase/_version.py +0 -1
  13. {meterbase-0.5.0 → meterbase-0.6.0}/.gitignore +0 -0
  14. {meterbase-0.5.0 → meterbase-0.6.0}/LICENSE +0 -0
  15. {meterbase-0.5.0 → meterbase-0.6.0}/pyproject.toml +0 -0
  16. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/__init__.py +0 -0
  17. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/__init__.py +0 -0
  18. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/_io.py +0 -0
  19. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/client.py +0 -0
  20. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/__init__.py +0 -0
  21. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customer_allowances.py +0 -0
  22. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customer_periods.py +0 -0
  23. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customer_plan.py +0 -0
  24. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customers.py +0 -0
  25. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/meters.py +0 -0
  26. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/plans.py +0 -0
  27. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/purchases.py +0 -0
  28. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_json.py +0 -0
  29. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_keys.py +0 -0
  30. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_models.py +0 -0
  31. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_not_given.py +0 -0
  32. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/__init__.py +0 -0
  33. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/_io.py +0 -0
  34. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/client.py +0 -0
  35. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/__init__.py +0 -0
  36. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customer_allowances.py +0 -0
  37. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customer_periods.py +0 -0
  38. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customer_plan.py +0 -0
  39. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customers.py +0 -0
  40. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/meters.py +0 -0
  41. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/plans.py +0 -0
  42. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/purchases.py +0 -0
  43. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_time.py +0 -0
  44. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/errors.py +0 -0
  45. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/py.typed +0 -0
  46. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/resources.py +0 -0
  47. {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meterbase
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Python SDK for the Meterbase engine
5
5
  Project-URL: Homepage, https://meterbase.tech
6
6
  Project-URL: Documentation, https://meterbase.tech/docs
@@ -756,14 +756,14 @@ Once the workspace has connected Stripe or Dodo Payments under **Payments** in
756
756
  the dashboard, and bound its plans and packs to Stripe prices or Dodo products,
757
757
  Meterbase sells them: it opens the checkout, receives the provider's webhooks,
758
758
  and moves the customer's plan or grants the pack. There is no webhook handler of
759
- yours in between. These calls go to the service at
760
- `https://service.meterbase.tech` rather than the engine, with the same API key;
759
+ yours in between. These calls go to the billing service at
760
+ `https://billing.meterbase.tech` rather than the engine, with the same API key;
761
761
  see [Options](#options). There each API key has three limits, whichever
762
- customers the calls are for: `billing.checkout` and `billing.state` have 30
763
- requests per 10 s each, and `purchases` has 120 per 10 s. All calls from one IP
764
- address share 300 per 10 s on top. A 429 there raises `RateLimitError` with
765
- `retry_after`, once the retries a `GET` or a keyed checkout gets have run out;
766
- `purchases.wait` keeps waiting instead.
762
+ customers the calls are for: `billing.checkout`, `change`, `cancel`, `keep` and
763
+ `portal` share 30 requests per 10 s, `billing.state` has 30 per 10 s of its own,
764
+ and `purchases` has 120 per 10 s. All calls from one IP address share 300 per
765
+ 10 s on top. A 429 there raises `RateLimitError` with `retry_after`, once the
766
+ call's retries have run out; `purchases.wait` keeps waiting instead.
767
767
 
768
768
  A Dodo Payments subscription made outside Meterbase's checkout, from a buy link
769
769
  or your own server, is held until you link it to its customer once under
@@ -802,31 +802,16 @@ A plan's checkout opens a new subscription, so it is for a customer without a
802
802
  paying one. For a customer whose subscription pays and is not cancelling
803
803
  (`billing.status` is `active`, `trialing` or `past_due` in their
804
804
  [state](#a-customers-billing), and `cancel_at_period_end` is false), it raises
805
- `ConflictError` with `code == "already_subscribed"`. There is no plan-change
806
- call yet: a customer who already pays changes plan or interval in the provider's
807
- customer portal, so link there for their `upgrade`, `downgrade` and
808
- `switch_interval` offers. A customer whose subscription is cancelling can check
809
- out a new one, which takes over when the old one ends; its purchase reads `held`
810
- until then.
811
-
812
- Set the portal up once per mode, on the provider's side, and give it only the
813
- prices or products you bound: a subscription moved onto any other is held for
814
- review under **Payments**.
815
-
816
- - **Stripe:** under Settings → Billing → Customer portal, turn on **Switch
817
- plan**, add the bound products and prices, and save. Then **Activate link**,
818
- and link to the login link it gives (`https://billing.stripe.com/p/login/…`),
819
- where the customer signs in with the email they paid with. In test mode,
820
- open a customer's portal from their page in the Stripe Dashboard.
821
- - **Dodo Payments:** put the bound subscription products in one Product
822
- Collection, since the portal changes plans only within one, and turn on
823
- **Allow Subscription Updates** under Settings → Subscriptions. Link to
824
- `https://customer.dodopayments.com/login/{business_id}`, where the customer
825
- asks for a sign-in link by the email they paid with. Test mode sends no
826
- sign-in emails, so there your server makes a session with your Dodo key:
827
- `POST /customers/{customer_id}/customer-portal/session`, with the
828
- `customer.customer_id` of `GET /subscriptions/{id}`, for the
829
- `billing.subscription_id` in their [state](#a-customers-billing).
805
+ `ConflictError` with `code == "already_subscribed"`: move them with
806
+ [`billing.change`](#change-cancel-keep-portal) instead, as their plan offers'
807
+ `via == "change"` says. A customer whose subscription is cancelling can check
808
+ out a new one, which takes over when the old one ends, its purchase reading
809
+ `held` until then. That is a second subscription, charged from the start, so
810
+ `keep` and then `change` is the way to move them.
811
+
812
+ A plan changed in the provider's own portal or dashboard still reaches the
813
+ ledger through the provider's event, as long as it moves onto a bound price: a
814
+ subscription moved onto any other is held for review under **Payments**.
830
815
 
831
816
  A checkout is never retried unless it is given an `idempotency_key`, 1 to 200
832
817
  printable ASCII characters with no spaces. Until the session expires, a repeat
@@ -891,7 +876,7 @@ state.plan # BillingPlan(key="pro", name="Pro"), or None
891
876
  if state.billing and state.billing.status == "past_due":
892
877
  print("Payment failing, next retry:", state.billing.next_retry_at) # None on Dodo
893
878
  for offer in state.offers:
894
- print(offer.action, offer.plan or offer.pack, offer.price.unit_amount)
879
+ print(offer.action, offer.via, offer.plan or offer.pack, offer.price.unit_amount)
895
880
  ```
896
881
 
897
882
  One read for a pricing or account page: the plan and its `period`, a change
@@ -906,30 +891,44 @@ what to buy before they buy. A customer that was deleted raises `NotFoundError`
906
891
  `billing.status` reads the same on both providers, with a cancelled or expired
907
892
  subscription `ended`. Dodo Payments has no `trialing`, since a trial reads
908
893
  `active`, and its `on_hold` reads as `unpaid`; `next_retry_at` and `lookup_key`
909
- are always `None` there.
894
+ are always `None` there. `billing.scheduled_change` is a plan change the
895
+ provider has scheduled, with `plan` `None` when the scheduled price is not
896
+ bound. `pending_change` is the ledger's: while the ledger's period does not end
897
+ with the provider's, it stays `None`, and only `scheduled_change` shows the
898
+ change.
910
899
 
911
900
  An offer's `action` is `current`, `switch_interval` for the same plan billed
912
901
  over another interval, `upgrade` or `downgrade` by price per month against the
913
- current plan, or `purchase` for a pack. A plan offer is sold to a customer
914
- without a paying subscription with `checkout(..., plan=offer.plan,
915
- price=offer.price.id)`, and a pack offer with `pack=offer.pack`. New actions can
916
- be added; treat one you do not know as unavailable. `price.unit_amount` is the
917
- price as listed, in the currency's minor unit, cents for USD. Dodo Payments adds
918
- tax at checkout, by the buyer's location, unless the product's price is tax
919
- inclusive, so a Dodo Payments buyer can pay more than the offer shows.
920
-
921
- A pack offer's `grant` is what one pack gives: `amount` on the meter whose key
922
- is `meter`, twice that for `quantity=2`, expiring `expires_days` after the
923
- payment, or never when that is `None`. A plan offer's `grant` is `None`. Choose
924
- a pack by its grant rather than by name, telling two packs on one meter apart
925
- by `amount`: Dodo Payments has no lookup keys, and its product ids differ
926
- between test and live mode.
902
+ current plan, or `purchase` for a pack. `via` names the call that buys it:
903
+ `change` for a plan offer while the customer's subscription pays, cancelling
904
+ included, `checkout` otherwise (a Dodo Payments subscription that ends by its
905
+ term too), and `None` for `current`. A plan offer is sold
906
+ with `plan=offer.plan, price=offer.price.id`, and a pack offer with
907
+ `pack=offer.pack`. New actions can be added; treat one you do not know as
908
+ unavailable. `price.unit_amount` is the price as listed, in the currency's minor
909
+ unit, cents for USD. Dodo Payments adds tax at checkout, by the buyer's
910
+ location, unless the product's price is tax inclusive, so a Dodo Payments buyer
911
+ can pay more than the offer shows.
912
+
913
+ A pack offer's `grants` is what one pack gives, one entry per meter: `amount`
914
+ on the meter whose key is `meter`, twice that for `quantity=2`, expiring
915
+ `expires_days` after the payment, or never when that is `None`. A plan offer's
916
+ `grants` is empty. Choose a pack by its grants rather than by name, telling two
917
+ packs on one meter apart by `amount`: Dodo Payments has no lookup keys, and its
918
+ product ids differ between test and live mode.
919
+
920
+ A pack can grant up to five meters at once, such as 500,000 `ai_tokens` and 50
921
+ `credits` for one price, each with its own expiry. A payment grants every meter
922
+ in the pack, and a refund takes back the refunded share of each. `grant` is the
923
+ first entry of `grants`, `None` on a plan offer, and is deprecated: it shows
924
+ one meter of a pack that may grant several.
927
925
 
928
926
  ```python
929
927
  top_up = next(
930
- (o for o in state.offers if o.grant and o.grant.meter == "ai_tokens"), None
928
+ (o for o in state.offers if [g.meter for g in o.grants] == ["ai_tokens"]),
929
+ None,
931
930
  )
932
- # top_up.grant == {"meter": "ai_tokens", "amount": 200_000, "expires_days": 90}
931
+ # top_up.grants == [{"meter": "ai_tokens", "amount": 200_000, "expires_days": 90}]
933
932
 
934
933
  if top_up and top_up.pack:
935
934
  session = meterbase.billing.checkout(
@@ -939,23 +938,139 @@ if top_up and top_up.pack:
939
938
  )
940
939
  ```
941
940
 
941
+ On Stripe, a plan can bill usage past a soft cap (`allow_overage`): Meterbase
942
+ adds it as a line to the customer's next renewal invoice. An allowance's
943
+ `overage_used` is the usage past `amount` this period, `None` under `no_cap`,
944
+ and `overage_price` the price per unit it is billed at, `None` when none is
945
+ bound. A plan offer's `overage` lists that plan's overage prices by meter, so a
946
+ pricing page can show them before a purchase. Dodo Payments bills no overage
947
+ from Meterbase yet, so there both are `None` and `[]`. An overage price is often
948
+ below one minor unit, so read its `unit_amount_decimal`, the amount in minor
949
+ units as a decimal string: $2 per million tokens is `"0.0002"`, and its
950
+ `unit_amount` is `None`.
951
+
952
+ ### Change, cancel, keep, portal
953
+
954
+ For a customer whose subscription pays, `billing.change` moves it to another
955
+ plan, or to the same plan at another interval, on the provider. Meterbase then
956
+ brings the ledger along at once, before the provider's own event arrives.
957
+
958
+ ```python
959
+ change = meterbase.billing.change(
960
+ "acct_1",
961
+ plan="scale", # the plan's key
962
+ # price="scale_yearly", a plan offer's price.id or price.lookup_key
963
+ # when="now" or "period_end"
964
+ )
965
+
966
+ if change.outcome == "declined":
967
+ ... # the charge was refused and nothing changed; change.declined says why
968
+ ```
969
+
970
+ Without `price`, the plan's price at the subscription's current interval is
971
+ used, else its first bound price. `action` is what the plan offer shows, and
972
+ sets when the change happens unless `when` says otherwise:
973
+
974
+ | `action` | `when` by default |
975
+ | ----------------- | ------------------------------------------------------------- |
976
+ | `upgrade` | `"now"`, charging the difference at once |
977
+ | `downgrade` | `"period_end"` |
978
+ | `switch_interval` | `"now"` to a longer interval, `"period_end"` to a shorter one |
979
+
980
+ | `outcome` | Means |
981
+ | ----------- | ---------------------------------------------------------------------------------------------------------- |
982
+ | `changed` | the subscription is on the new price now |
983
+ | `scheduled` | the change happens at `effective_at`, the period end |
984
+ | `pending` | Dodo Payments took the change and is still charging for it; the plan moves once the payment succeeds |
985
+ | `declined` | Stripe refused the charge, and nothing changed: `declined` is `card_declined` or `authentication_required` |
986
+ | `unchanged` | the subscription is already on that price |
987
+ | `preview` | `preview=True`: what the change would do, with nothing written |
988
+
989
+ `due_now` is what the provider takes now, in minor units; a negative amount is a
990
+ credit to the customer's balance on Stripe. It is `None` for a change at the
991
+ period end, and on Dodo Payments after a real change, which Dodo answers with no
992
+ amount; a preview has it on both. An upgrade is charged off session to the saved
993
+ payment method, so a card that asks for 3D Secure answers `declined` with
994
+ `authentication_required`. `ledger_updated` is true when `billing.state` already
995
+ shows the result; after `changed` or `scheduled`, a false means the provider's
996
+ event brings it there.
997
+
998
+ `change`, `cancel`, `keep` and `portal` are safe to repeat, so the SDK retries
999
+ them as it does a `GET`. A change already made answers `unchanged`, or
1000
+ `scheduled` for the same change at the period end, and nothing is charged twice.
1001
+
1002
+ `billing.cancel` ends the subscription at the period end, `effective_at`, and
1003
+ never at once. Until then the customer keeps their plan, and then moves to the
1004
+ fallback plan set under **Payments**; without one, that event waits there for
1005
+ review. A cancel already set answers `unchanged`. Usage past a soft cap in this
1006
+ last period is recorded, not invoiced, so use a hard cap where that matters.
1007
+
1008
+ `billing.keep` drops a scheduled plan change or cancel, on the provider and then
1009
+ in the ledger, and answers `unchanged` when nothing was scheduled. While either
1010
+ is scheduled (`billing.scheduled_change`, or `cancel_at_period_end`), `change`
1011
+ and `cancel` raise `ConflictError` with `code == "change_scheduled"` rather than
1012
+ replace it, so a cancelling customer who wants another plan takes two calls:
1013
+
1014
+ ```python
1015
+ meterbase.billing.keep("acct_1")
1016
+ meterbase.billing.change("acct_1", plan="scale")
1017
+ ```
1018
+
1019
+ On Dodo Payments, an upgrade answers `pending` while its charge runs: read
1020
+ `billing.state` afterwards to see whether the plan moved, since a failed charge
1021
+ leaves it as it was and nothing records the attempt. Another change meanwhile
1022
+ raises `ConflictError` with `code == "change_pending"`. `cancel_at_period_end`
1023
+ is also true there for a subscription that ends by its own term, which `keep`
1024
+ cannot clear: `change` raises `ConflictError` with `code == "ends_by_term"`, and
1025
+ `cancel` answers `unchanged`. Start a new subscription with `checkout`; it takes
1026
+ over when this one ends.
1027
+
1028
+ `billing.portal` makes a session in the provider's customer portal, where the
1029
+ customer updates their payment method, reads invoices and cancels:
1030
+
1031
+ ```python
1032
+ portal = meterbase.billing.portal(
1033
+ "acct_1", return_url="https://app.example.com/account"
1034
+ )
1035
+ redirect(portal.url) # now
1036
+ ```
1037
+
1038
+ `return_url` is required: https, or http on localhost. On Stripe the session
1039
+ uses a portal setup named Meterbase, which Meterbase adds to the account once:
1040
+ payment methods, invoices, billing details, and cancelling at the period end,
1041
+ with switching plans left to `change`. A Stripe session expires 5 minutes after
1042
+ it is made, and once opened an hour after its last use, so make one when the
1043
+ customer asks and redirect at once. A cancel in the Stripe portal may fail
1044
+ while a change is scheduled; call `keep`, then `billing.cancel`. On Dodo
1045
+ Payments the portal is set up in Dodo, and a session lasts 24 hours. A customer
1046
+ who has not paid through the provider yet has no portal, and raises
1047
+ `ConflictError` with `code == "no_provider_customer"`.
1048
+
942
1049
  ### Billing errors
943
1050
 
944
- | `code` | Raises | Calls | When |
945
- | ------------------------ | -------------------------------- | ------------------- | ------------------------------------------------------------------------ |
946
- | `unauthenticated` | `AuthenticationError`, 401 | all | unknown, revoked or malformed key |
947
- | `payments_not_connected` | `ConflictError`, 409 | all | the workspace has no payment provider connected |
948
- | `already_subscribed` | `ConflictError`, 409 | `checkout` | a plan checkout for a customer who already pays |
949
- | `customer_deleted` | `ConflictError`, 409 | `checkout` | the customer was deleted |
950
- | `idempotency_key_reused` | `IdempotencyKeyReusedError`, 409 | `checkout` | the `idempotency_key` was used for a different checkout |
951
- | `customer_not_found` | `NotFoundError`, 404 | `state` | the customer was deleted |
952
- | `purchase_not_found` | `NotFoundError`, 404 | `purchases` | no checkout has that session id; `wait` reads it as waiting |
953
- | `price_not_bound` | `InvalidRequestError`, 422 | `checkout` | the plan or pack has nothing bound, or `price` is not bound to the plan |
954
- | `invalid_request` | `InvalidRequestError`, 422 | `checkout`, `state` | a field failed validation; the message names it |
955
- | `rate_limited` | `RateLimitError`, 429 | all | past a [limit](#billing), with `retry_after`; `wait` keeps waiting |
956
- | `provider_error` | `ServerError`, 502 | all | the provider refused or failed; the message says which |
957
- | `unavailable` | `ServerError`, 503 | all | the connected key could not be opened: disconnect, then connect it again |
958
- | `engine_unavailable` | `ServerError`, 503 | all | Meterbase could not be reached |
1051
+ | `code` | Raises | Calls | When |
1052
+ | ------------------------ | -------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
1053
+ | `unauthenticated` | `AuthenticationError`, 401 | all | unknown, revoked or malformed key |
1054
+ | `payments_not_connected` | `ConflictError`, 409 | all | the workspace has no payment provider connected |
1055
+ | `payments_not_live` | `ConflictError`, 409 | `checkout`, `change`, `cancel`, `keep`, `portal` | a live workspace that has not finished Go live in the dashboard, or that Meterbase has closed |
1056
+ | `already_subscribed` | `ConflictError`, 409 | `checkout` | a plan checkout for a customer who already pays: use `change` |
1057
+ | `customer_deleted` | `ConflictError`, 409 | `checkout` | the customer was deleted |
1058
+ | `idempotency_key_reused` | `IdempotencyKeyReusedError`, 409 | `checkout` | the `idempotency_key` was used for a different checkout |
1059
+ | `not_subscribed` | `ConflictError`, 409 | `change`, `cancel`, `keep` | the customer has no active subscription through the provider, or its price is bound to no plan |
1060
+ | `payment_failing` | `ConflictError`, 409 | `change`, `cancel` | the last payment failed: send the customer to `portal` to update their card, then try again |
1061
+ | `change_scheduled` | `ConflictError`, 409 | `change`, `cancel` | a plan change or a cancel is already scheduled: call `keep` first |
1062
+ | `change_pending` | `ConflictError`, 409 | `change` | Dodo Payments: a plan change is waiting for its payment; read `state`, then try again |
1063
+ | `ends_by_term` | `ConflictError`, 409 | `change` | Dodo Payments: the subscription ends by its term; start a new one with `checkout` |
1064
+ | `customer_busy` | `ConflictError`, 409 | `change`, `cancel`, `keep` | another change for the customer is running; try again in a moment |
1065
+ | `no_provider_customer` | `ConflictError`, 409 | `portal` | the customer has not paid through the provider yet |
1066
+ | `customer_not_found` | `NotFoundError`, 404 | all but `checkout` and `purchases` | the customer was deleted |
1067
+ | `purchase_not_found` | `NotFoundError`, 404 | `purchases` | no checkout has that session id; `wait` reads it as waiting |
1068
+ | `price_not_bound` | `InvalidRequestError`, 422 | `checkout`, `change` | the plan or pack has nothing bound, or `price` is not bound to the plan |
1069
+ | `invalid_request` | `InvalidRequestError`, 422 | all but `purchases` | a field failed validation; the message names it |
1070
+ | `rate_limited` | `RateLimitError`, 429 | all | past a [limit](#billing), with `retry_after`; `wait` keeps waiting |
1071
+ | `provider_error` | `ServerError`, 502 | all | the provider refused or failed; the message says which |
1072
+ | `unavailable` | `ServerError`, 503 | all | the connected key could not be opened: disconnect, then connect it again |
1073
+ | `engine_unavailable` | `ServerError`, 503 | all | Meterbase could not be reached |
959
1074
 
960
1075
  ## Webhooks
961
1076
 
@@ -1133,7 +1248,7 @@ except RateLimitError as error:
1133
1248
  | `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7 |
1134
1249
  | `TooLateError` | 422 — an `InvalidRequestError`: the key is over an hour from the engine's clock, which it carries on `server_time`. Nothing was recorded |
1135
1250
  | `ReservationMismatchError` | 422 — an `InvalidRequestError`: the reservation was made for a different customer or meter |
1136
- | `RateLimitError` | 429 — `check` or `reserve` past the workspace's fair-use rate, `code == "rate_limited"`, with `retry_after` in seconds. `reserve` retries it for you; `check` raises at once, so fail open or closed. Also a billing call past its [per-key limit](#billing), once a `GET` or a keyed checkout has retried it; `purchases.wait` keeps waiting |
1251
+ | `RateLimitError` | 429 — `check` or `reserve` past the workspace's fair-use rate, `code == "rate_limited"`, with `retry_after` in seconds. `reserve` retries it for you; `check` raises at once, so fail open or closed. Also a billing call past its [per-key limit](#billing), once the call's retries have run out; `purchases.wait` keeps waiting |
1137
1252
  | `ServerError` | 5xx |
1138
1253
  | `APIConnectionError` / `APITimeoutError` | the request never got an answer |
1139
1254
  | `WebhookVerificationError` | `verify_webhook`: not a delivery Meterbase signed in the last five minutes |
@@ -1153,7 +1268,7 @@ SDK would raise for a response, for realistic errors in your own tests.
1153
1268
  Meterbase(
1154
1269
  api_key="mb_sk_live_…",
1155
1270
  base_url="https://api.meterbase.tech",
1156
- billing_base_url="https://service.meterbase.tech", # billing and purchases
1271
+ billing_base_url="https://billing.meterbase.tech", # billing and purchases
1157
1272
  timeout=10.0, # seconds, per attempt, on every call; unset, 35, and 3 for check
1158
1273
  max_retries=2,
1159
1274
  http_client=httpx.Client(proxy="http://localhost:8030"), # yours to close
@@ -1168,7 +1283,7 @@ there — failing open or failing closed — rather than leaving the user waitin
1168
1283
  out the 35 s the writes get. Pass an `http_client` for proxies,
1169
1284
  limits or a transport of your own: an `httpx.Client` for `Meterbase`, an
1170
1285
  `httpx.AsyncClient` for `AsyncMeterbase`. `billing` and `purchases` go to
1171
- `billing_base_url`, `https://service.meterbase.tech` unless set, and every other
1286
+ `billing_base_url`, `https://billing.meterbase.tech` unless set, and every other
1172
1287
  call to `base_url`, over the same connections.
1173
1288
 
1174
1289
  For other settings on some calls, derive a client with them. It shares the
@@ -1188,15 +1303,18 @@ cancelled `track` cannot record usage twice. A sync call cannot be cancelled;
1188
1303
  bound it with `timeout` and `max_retries` instead.
1189
1304
 
1190
1305
  **Retries** apply to `GET`, `DELETE`, `track`, `reserve`, `customers.ensure`,
1191
- and a grant, plan change or `billing.checkout` given an `idempotency_key`, on
1192
- connection failures, timeouts, 408, 429 and 5xx, with exponential backoff and
1193
- full jitter. `Retry-After` is honoured when Meterbase sends it, up to eight
1194
- seconds. Every other `POST` and `PATCH` is never replayed, because a retried
1195
- create could produce a second row. An `ensure` can be, because running it twice
1196
- changes nothing: one whose first attempt landed answers `created` false. So can
1197
- a keyed grant or plan change, which the engine answers with what the first
1198
- attempt wrote: one whose first attempt landed answers `replayed` true. A keyed
1199
- checkout is answered by Meterbase with the session its first attempt opened.
1306
+ `billing.change`, `cancel`, `keep` and `portal`, and a grant, plan change or
1307
+ `billing.checkout` given an `idempotency_key`, on connection failures, timeouts,
1308
+ 408, 429 and 5xx, with exponential backoff and full jitter. `Retry-After` is
1309
+ honoured when Meterbase sends it, up to eight seconds. Every other `POST` and
1310
+ `PATCH` is never replayed, because a retried create could produce a second row.
1311
+ An `ensure` can be, because running it twice changes nothing: one whose first
1312
+ attempt landed answers `created` false. So can a keyed grant or plan change,
1313
+ which the engine answers with what the first attempt wrote: one whose first
1314
+ attempt landed answers `replayed` true. A keyed checkout is answered by
1315
+ Meterbase with the session its first attempt opened. So is a `billing.change`,
1316
+ which Meterbase checks against the subscription itself: one whose first attempt
1317
+ landed answers `unchanged`, or `scheduled` at the period end.
1200
1318
 
1201
1319
  `track` and `reserve` are the exception because they carry an idempotency key,
1202
1320
  and because the SDK fixes that key **before** the retry loop starts: every
@@ -1225,11 +1343,19 @@ Pre-1.0, the minor version is where breaking changes land — deliberately, so a
1225
1343
  change that will not run against your code cannot arrive as a patch. The SDK
1226
1344
  follows the engine's API, as the TypeScript SDK (`mbase-sdk`) does.
1227
1345
 
1346
+ **0.6.0** adds `billing.change`, `billing.cancel`, `billing.keep` and
1347
+ `billing.portal`, and `billing_base_url` now defaults to
1348
+ `https://billing.meterbase.tech`. The old host still answers, so nothing in it
1349
+ breaks code written for 0.5.
1350
+
1351
+ **0.5.1** adds `grants` to a billing offer: every meter a pack grants. `grant`,
1352
+ the first of them, stays and is deprecated.
1353
+
1228
1354
  **0.5.0** adds `billing` and `purchases`, for a workspace that sells through
1229
1355
  Stripe or Dodo Payments: `billing.checkout`, `billing.state`,
1230
1356
  `purchases.retrieve` and `purchases.wait`, with the `billing_base_url` option.
1231
- They go to the service at `https://service.meterbase.tech` rather than the
1232
- engine. Nothing in it breaks code written for 0.4.0.
1357
+ They go to the Meterbase service rather than the engine. Nothing in it breaks
1358
+ code written for 0.4.0.
1233
1359
 
1234
1360
  **0.4.0** pages the customer list, and adds `customers.list_all` and the
1235
1361
  `limit`, `starting_after`, `plan` and `has_plan` parameters. One change can break