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.
- {meterbase-0.5.0 → meterbase-0.6.0}/PKG-INFO +206 -80
- {meterbase-0.5.0 → meterbase-0.6.0}/README.md +205 -79
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/_transport.py +6 -5
- meterbase-0.6.0/src/meterbase/_async/resources/billing.py +160 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_http.py +1 -1
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/_transport.py +6 -5
- meterbase-0.6.0/src/meterbase/_sync/resources/billing.py +164 -0
- meterbase-0.6.0/src/meterbase/_version.py +1 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/types.py +110 -7
- meterbase-0.5.0/src/meterbase/_async/resources/billing.py +0 -73
- meterbase-0.5.0/src/meterbase/_sync/resources/billing.py +0 -77
- meterbase-0.5.0/src/meterbase/_version.py +0 -1
- {meterbase-0.5.0 → meterbase-0.6.0}/.gitignore +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/LICENSE +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/pyproject.toml +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/__init__.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/__init__.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/_io.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/client.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/__init__.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customer_allowances.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customer_periods.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customer_plan.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/customers.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/meters.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/plans.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_async/resources/purchases.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_json.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_keys.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_models.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_not_given.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/__init__.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/_io.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/client.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/__init__.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customer_allowances.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customer_periods.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customer_plan.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/customers.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/meters.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/plans.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_sync/resources/purchases.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/_time.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/errors.py +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/py.typed +0 -0
- {meterbase-0.5.0 → meterbase-0.6.0}/src/meterbase/resources.py +0 -0
- {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.
|
|
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://
|
|
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`
|
|
763
|
-
requests per 10 s
|
|
764
|
-
|
|
765
|
-
|
|
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"
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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.
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
928
|
+
(o for o in state.offers if [g.meter for g in o.grants] == ["ai_tokens"]),
|
|
929
|
+
None,
|
|
931
930
|
)
|
|
932
|
-
# top_up.
|
|
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
|
|
945
|
-
| ------------------------ | -------------------------------- |
|
|
946
|
-
| `unauthenticated` | `AuthenticationError`, 401 | all
|
|
947
|
-
| `payments_not_connected` | `ConflictError`, 409 | all
|
|
948
|
-
| `
|
|
949
|
-
| `
|
|
950
|
-
| `
|
|
951
|
-
| `
|
|
952
|
-
| `
|
|
953
|
-
| `
|
|
954
|
-
| `
|
|
955
|
-
| `
|
|
956
|
-
| `
|
|
957
|
-
| `
|
|
958
|
-
| `
|
|
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
|
|
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://
|
|
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://
|
|
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
|
|
1192
|
-
|
|
1193
|
-
full jitter. `Retry-After` is
|
|
1194
|
-
seconds. Every other `POST` and
|
|
1195
|
-
create could produce a second row.
|
|
1196
|
-
|
|
1197
|
-
a keyed grant or plan change,
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
|
1232
|
-
|
|
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
|