meterbase 0.4.1__tar.gz → 0.5.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.4.1 → meterbase-0.5.0}/PKG-INFO +230 -13
- {meterbase-0.4.1 → meterbase-0.5.0}/README.md +229 -12
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/_transport.py +20 -8
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/client.py +20 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/__init__.py +4 -0
- meterbase-0.5.0/src/meterbase/_async/resources/billing.py +73 -0
- meterbase-0.5.0/src/meterbase/_async/resources/purchases.py +73 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_http.py +8 -5
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_models.py +1 -1
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/_transport.py +20 -8
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/client.py +20 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/__init__.py +4 -0
- meterbase-0.5.0/src/meterbase/_sync/resources/billing.py +77 -0
- meterbase-0.5.0/src/meterbase/_sync/resources/purchases.py +77 -0
- meterbase-0.5.0/src/meterbase/_version.py +1 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/errors.py +10 -5
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/resources.py +8 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/types.py +187 -0
- meterbase-0.4.1/src/meterbase/_version.py +0 -1
- {meterbase-0.4.1 → meterbase-0.5.0}/.gitignore +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/LICENSE +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/pyproject.toml +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/__init__.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/__init__.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/_io.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/customer_allowances.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/customer_periods.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/customer_plan.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/customers.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/meters.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_async/resources/plans.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_json.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_keys.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_not_given.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/__init__.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/_io.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/customer_allowances.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/customer_periods.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/customer_plan.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/customers.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/meters.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_sync/resources/plans.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/_time.py +0 -0
- {meterbase-0.4.1 → meterbase-0.5.0}/src/meterbase/py.typed +0 -0
- {meterbase-0.4.1 → meterbase-0.5.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.5.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
|
|
@@ -750,6 +750,213 @@ for grant in paid.data: # the grant the payment made, or none
|
|
|
750
750
|
meterbase.customers.allowances.revoke(customer.id, grant.id)
|
|
751
751
|
```
|
|
752
752
|
|
|
753
|
+
## Billing
|
|
754
|
+
|
|
755
|
+
Once the workspace has connected Stripe or Dodo Payments under **Payments** in
|
|
756
|
+
the dashboard, and bound its plans and packs to Stripe prices or Dodo products,
|
|
757
|
+
Meterbase sells them: it opens the checkout, receives the provider's webhooks,
|
|
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;
|
|
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.
|
|
767
|
+
|
|
768
|
+
A Dodo Payments subscription made outside Meterbase's checkout, from a buy link
|
|
769
|
+
or your own server, is held until you link it to its customer once under
|
|
770
|
+
**Payments**: anyone with a buy link can name a customer, so Meterbase trusts
|
|
771
|
+
only a name its own checkout signed. Give each Dodo subscription product a
|
|
772
|
+
Subscription Period longer than its billing period, such as 10 years: with the
|
|
773
|
+
two equal, Dodo Payments ends the subscription after one charge.
|
|
774
|
+
|
|
775
|
+
### Checkout
|
|
776
|
+
|
|
777
|
+
```python
|
|
778
|
+
session = meterbase.billing.checkout(
|
|
779
|
+
"acct_1", # the tenant's own id
|
|
780
|
+
plan="pro", # the plan's key; or pack="tokens_200k", quantity=2
|
|
781
|
+
success_url="https://app.example.com/billing/done",
|
|
782
|
+
cancel_url="https://app.example.com/pricing",
|
|
783
|
+
)
|
|
784
|
+
redirect(session.url)
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
Give exactly one of `plan` or `pack`, and `quantity` only with a pack. A pack is
|
|
788
|
+
named as the pack offers from [`billing.state`](#a-customers-billing) name it:
|
|
789
|
+
by its price's lookup key, or by the price id when it has none. Dodo Payments
|
|
790
|
+
has no lookup keys, so there it is the product id, which differs between test
|
|
791
|
+
and live mode: take it from the offer, chosen by
|
|
792
|
+
[what it grants](#a-customers-billing). A plan bound at more than one interval
|
|
793
|
+
sells its first bound price, unless `price` names another: a plan offer's
|
|
794
|
+
`price.id` or `price.lookup_key`. `checkout` reuses the provider's customer
|
|
795
|
+
already linked to the customer, and a customer Meterbase has not seen yet is
|
|
796
|
+
created when their purchase is applied. `success_url` is https, or http on
|
|
797
|
+
localhost; Meterbase adds `session_id`, its own opaque id for the checkout, to
|
|
798
|
+
it. The session expires at `session.expires_at`: an hour after it opens on
|
|
799
|
+
Stripe, and 24 hours on Dodo Payments.
|
|
800
|
+
|
|
801
|
+
A plan's checkout opens a new subscription, so it is for a customer without a
|
|
802
|
+
paying one. For a customer whose subscription pays and is not cancelling
|
|
803
|
+
(`billing.status` is `active`, `trialing` or `past_due` in their
|
|
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).
|
|
830
|
+
|
|
831
|
+
A checkout is never retried unless it is given an `idempotency_key`, 1 to 200
|
|
832
|
+
printable ASCII characters with no spaces. Until the session expires, a repeat
|
|
833
|
+
with the same key answers that session rather than opening another when it
|
|
834
|
+
names the same customer, plan or pack, price and quantity, and raises
|
|
835
|
+
`IdempotencyKeyReusedError` (409 `idempotency_key_reused`) otherwise. The return
|
|
836
|
+
URLs stay the first request's. Once the session expires, the key opens a new
|
|
837
|
+
one. What each call can raise is under [Billing errors](#billing-errors).
|
|
838
|
+
|
|
839
|
+
### The return page
|
|
840
|
+
|
|
841
|
+
The provider can send the customer back before its webhook has arrived, so the
|
|
842
|
+
return page waits for the purchase to land. Dodo Payments adds query parameters
|
|
843
|
+
of its own, such as `status`; they prove nothing about payment, so read the
|
|
844
|
+
purchase by `session_id`:
|
|
845
|
+
|
|
846
|
+
```python
|
|
847
|
+
purchase = meterbase.purchases.wait(request.args["session_id"], timeout=15)
|
|
848
|
+
|
|
849
|
+
if purchase is None:
|
|
850
|
+
... # no checkout has that session_id
|
|
851
|
+
elif purchase.state in ("waiting", "settling"):
|
|
852
|
+
... # still on its way: show "Confirming your payment"
|
|
853
|
+
elif purchase.state in ("fulfilled", "late", "held"):
|
|
854
|
+
... # paid; only "fulfilled" means the plan or the pack is in place
|
|
855
|
+
else:
|
|
856
|
+
... # "failed" or "expired": nothing was bought
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
| `state` | Meaning |
|
|
860
|
+
| ----------- | ------------------------------------------------------------------------- |
|
|
861
|
+
| `waiting` | the checkout is open, or it was paid and is not applied yet |
|
|
862
|
+
| `settling` | paid by a method that clears later, such as a bank debit |
|
|
863
|
+
| `fulfilled` | the plan or the pack is in place |
|
|
864
|
+
| `late` | paid over 90 s ago and still not applied: the customer can close the page |
|
|
865
|
+
| `held` | paid, and held for review under **Payments** in the dashboard |
|
|
866
|
+
| `failed` | the payment failed |
|
|
867
|
+
| `expired` | the checkout closed unpaid |
|
|
868
|
+
|
|
869
|
+
`wait` reads the purchase every `interval` seconds, 2 unless set, for up to
|
|
870
|
+
`timeout` seconds, 90 unless set, and returns as soon as the state is anything
|
|
871
|
+
but `waiting` or `settling`. Running out of time is not an error: it returns
|
|
872
|
+
the last read, so show a purchase still pending as pending, and read it again
|
|
873
|
+
from the page. Many hosts cut a request off before 90 s, so a wait inside the
|
|
874
|
+
page's request passes a shorter `timeout`, as above. It returns `None` when the
|
|
875
|
+
purchase was never found, which means no checkout has that id, as with an
|
|
876
|
+
edited link. A 404 reads as waiting, in case the provider does not show a new
|
|
877
|
+
session yet. A `RateLimitError` reads as waiting too, with the next read after
|
|
878
|
+
its `retry_after` when that is longer than `interval`, since every return page
|
|
879
|
+
on one key shares the 120 reads per 10 s.
|
|
880
|
+
`timeout` bounds the polling, not each read, which keeps the client's own
|
|
881
|
+
timeout and retries. A negative or NaN `timeout`, an `interval` that is not
|
|
882
|
+
above 0, or an empty session id raises `MeterbaseError` and sends nothing.
|
|
883
|
+
`purchases.retrieve` reads it once.
|
|
884
|
+
|
|
885
|
+
### A customer's billing
|
|
886
|
+
|
|
887
|
+
```python
|
|
888
|
+
state = meterbase.billing.state("acct_1")
|
|
889
|
+
|
|
890
|
+
state.plan # BillingPlan(key="pro", name="Pro"), or None
|
|
891
|
+
if state.billing and state.billing.status == "past_due":
|
|
892
|
+
print("Payment failing, next retry:", state.billing.next_retry_at) # None on Dodo
|
|
893
|
+
for offer in state.offers:
|
|
894
|
+
print(offer.action, offer.plan or offer.pack, offer.price.unit_amount)
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
One read for a pricing or account page: the plan and its `period`, a change
|
|
898
|
+
waiting to land (`pending_change`), each meter the plan meters (`allowances`),
|
|
899
|
+
the open grants beside it, purchased packs included (`grants`), the provider's
|
|
900
|
+
subscription the plan follows (`billing`, `None` when there is none), and every
|
|
901
|
+
bound price on offer. A customer Meterbase has not seen yet reads as one with no
|
|
902
|
+
plan, every plan offer an `upgrade`, so a pricing page can show a new visitor
|
|
903
|
+
what to buy before they buy. A customer that was deleted raises `NotFoundError`
|
|
904
|
+
(404 `customer_not_found`).
|
|
905
|
+
|
|
906
|
+
`billing.status` reads the same on both providers, with a cancelled or expired
|
|
907
|
+
subscription `ended`. Dodo Payments has no `trialing`, since a trial reads
|
|
908
|
+
`active`, and its `on_hold` reads as `unpaid`; `next_retry_at` and `lookup_key`
|
|
909
|
+
are always `None` there.
|
|
910
|
+
|
|
911
|
+
An offer's `action` is `current`, `switch_interval` for the same plan billed
|
|
912
|
+
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.
|
|
927
|
+
|
|
928
|
+
```python
|
|
929
|
+
top_up = next(
|
|
930
|
+
(o for o in state.offers if o.grant and o.grant.meter == "ai_tokens"), None
|
|
931
|
+
)
|
|
932
|
+
# top_up.grant == {"meter": "ai_tokens", "amount": 200_000, "expires_days": 90}
|
|
933
|
+
|
|
934
|
+
if top_up and top_up.pack:
|
|
935
|
+
session = meterbase.billing.checkout(
|
|
936
|
+
"acct_1",
|
|
937
|
+
pack=top_up.pack,
|
|
938
|
+
success_url="https://app.example.com/billing/done",
|
|
939
|
+
)
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### Billing errors
|
|
943
|
+
|
|
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 |
|
|
959
|
+
|
|
753
960
|
## Webhooks
|
|
754
961
|
|
|
755
962
|
Meterbase posts to your endpoints when a customer crosses a usage threshold,
|
|
@@ -920,13 +1127,13 @@ except RateLimitError as error:
|
|
|
920
1127
|
| `NotFoundError` | 404 |
|
|
921
1128
|
| `ConflictError` | 409 — a key or external id is taken |
|
|
922
1129
|
| `PeriodSettlingError` | 409 — a `ConflictError`: the period ended under a minute ago, so read it again after `retry_after` seconds |
|
|
923
|
-
| `IdempotencyKeyReusedError` | 409 — a `ConflictError`: the `idempotency_key` on a grant
|
|
1130
|
+
| `IdempotencyKeyReusedError` | 409 — a `ConflictError`: the `idempotency_key` on a grant, a plan change or a `billing.checkout` was already used for a different request. No grant or plan change was written, and no checkout opened |
|
|
924
1131
|
| `InvalidRequestError` | 422 — a field failed validation; or `400 invalid_query`, a query parameter that did |
|
|
925
1132
|
| `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart` |
|
|
926
1133
|
| `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7 |
|
|
927
1134
|
| `TooLateError` | 422 — an `InvalidRequestError`: the key is over an hour from the engine's clock, which it carries on `server_time`. Nothing was recorded |
|
|
928
1135
|
| `ReservationMismatchError` | 422 — an `InvalidRequestError`: the reservation was made for a different customer or meter |
|
|
929
|
-
| `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 |
|
|
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 |
|
|
930
1137
|
| `ServerError` | 5xx |
|
|
931
1138
|
| `APIConnectionError` / `APITimeoutError` | the request never got an answer |
|
|
932
1139
|
| `WebhookVerificationError` | `verify_webhook`: not a delivery Meterbase signed in the last five minutes |
|
|
@@ -945,7 +1152,8 @@ SDK would raise for a response, for realistic errors in your own tests.
|
|
|
945
1152
|
```python
|
|
946
1153
|
Meterbase(
|
|
947
1154
|
api_key="mb_sk_live_…",
|
|
948
|
-
base_url="https://api.meterbase.
|
|
1155
|
+
base_url="https://api.meterbase.tech",
|
|
1156
|
+
billing_base_url="https://service.meterbase.tech", # billing and purchases
|
|
949
1157
|
timeout=10.0, # seconds, per attempt, on every call; unset, 35, and 3 for check
|
|
950
1158
|
max_retries=2,
|
|
951
1159
|
http_client=httpx.Client(proxy="http://localhost:8030"), # yours to close
|
|
@@ -959,7 +1167,9 @@ milliseconds when the engine is well. Decide what an `APITimeoutError` means
|
|
|
959
1167
|
there — failing open or failing closed — rather than leaving the user waiting
|
|
960
1168
|
out the 35 s the writes get. Pass an `http_client` for proxies,
|
|
961
1169
|
limits or a transport of your own: an `httpx.Client` for `Meterbase`, an
|
|
962
|
-
`httpx.AsyncClient` for `AsyncMeterbase`.
|
|
1170
|
+
`httpx.AsyncClient` for `AsyncMeterbase`. `billing` and `purchases` go to
|
|
1171
|
+
`billing_base_url`, `https://service.meterbase.tech` unless set, and every other
|
|
1172
|
+
call to `base_url`, over the same connections.
|
|
963
1173
|
|
|
964
1174
|
For other settings on some calls, derive a client with them. It shares the
|
|
965
1175
|
original's connections, and its clock:
|
|
@@ -978,14 +1188,15 @@ cancelled `track` cannot record usage twice. A sync call cannot be cancelled;
|
|
|
978
1188
|
bound it with `timeout` and `max_retries` instead.
|
|
979
1189
|
|
|
980
1190
|
**Retries** apply to `GET`, `DELETE`, `track`, `reserve`, `customers.ensure`,
|
|
981
|
-
and a grant
|
|
982
|
-
timeouts, 408, 429 and 5xx, with exponential backoff and
|
|
983
|
-
`Retry-After` is honoured when
|
|
984
|
-
other `POST` and `PATCH` is never replayed, because a retried
|
|
985
|
-
produce a second row. An `ensure` can be, because running it twice
|
|
986
|
-
nothing: one whose first attempt landed answers `created` false. So can
|
|
987
|
-
grant or plan change, which the engine answers with what the first
|
|
988
|
-
wrote: one whose first attempt landed answers `replayed` true.
|
|
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.
|
|
989
1200
|
|
|
990
1201
|
`track` and `reserve` are the exception because they carry an idempotency key,
|
|
991
1202
|
and because the SDK fixes that key **before** the retry loop starts: every
|
|
@@ -1014,6 +1225,12 @@ Pre-1.0, the minor version is where breaking changes land — deliberately, so a
|
|
|
1014
1225
|
change that will not run against your code cannot arrive as a patch. The SDK
|
|
1015
1226
|
follows the engine's API, as the TypeScript SDK (`mbase-sdk`) does.
|
|
1016
1227
|
|
|
1228
|
+
**0.5.0** adds `billing` and `purchases`, for a workspace that sells through
|
|
1229
|
+
Stripe or Dodo Payments: `billing.checkout`, `billing.state`,
|
|
1230
|
+
`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.
|
|
1233
|
+
|
|
1017
1234
|
**0.4.0** pages the customer list, and adds `customers.list_all` and the
|
|
1018
1235
|
`limit`, `starting_after`, `plan` and `has_plan` parameters. One change can break
|
|
1019
1236
|
code: `customers.list()` answers one page of 100, newest first, where it used to
|
|
@@ -723,6 +723,213 @@ for grant in paid.data: # the grant the payment made, or none
|
|
|
723
723
|
meterbase.customers.allowances.revoke(customer.id, grant.id)
|
|
724
724
|
```
|
|
725
725
|
|
|
726
|
+
## Billing
|
|
727
|
+
|
|
728
|
+
Once the workspace has connected Stripe or Dodo Payments under **Payments** in
|
|
729
|
+
the dashboard, and bound its plans and packs to Stripe prices or Dodo products,
|
|
730
|
+
Meterbase sells them: it opens the checkout, receives the provider's webhooks,
|
|
731
|
+
and moves the customer's plan or grants the pack. There is no webhook handler of
|
|
732
|
+
yours in between. These calls go to the service at
|
|
733
|
+
`https://service.meterbase.tech` rather than the engine, with the same API key;
|
|
734
|
+
see [Options](#options). There each API key has three limits, whichever
|
|
735
|
+
customers the calls are for: `billing.checkout` and `billing.state` have 30
|
|
736
|
+
requests per 10 s each, and `purchases` has 120 per 10 s. All calls from one IP
|
|
737
|
+
address share 300 per 10 s on top. A 429 there raises `RateLimitError` with
|
|
738
|
+
`retry_after`, once the retries a `GET` or a keyed checkout gets have run out;
|
|
739
|
+
`purchases.wait` keeps waiting instead.
|
|
740
|
+
|
|
741
|
+
A Dodo Payments subscription made outside Meterbase's checkout, from a buy link
|
|
742
|
+
or your own server, is held until you link it to its customer once under
|
|
743
|
+
**Payments**: anyone with a buy link can name a customer, so Meterbase trusts
|
|
744
|
+
only a name its own checkout signed. Give each Dodo subscription product a
|
|
745
|
+
Subscription Period longer than its billing period, such as 10 years: with the
|
|
746
|
+
two equal, Dodo Payments ends the subscription after one charge.
|
|
747
|
+
|
|
748
|
+
### Checkout
|
|
749
|
+
|
|
750
|
+
```python
|
|
751
|
+
session = meterbase.billing.checkout(
|
|
752
|
+
"acct_1", # the tenant's own id
|
|
753
|
+
plan="pro", # the plan's key; or pack="tokens_200k", quantity=2
|
|
754
|
+
success_url="https://app.example.com/billing/done",
|
|
755
|
+
cancel_url="https://app.example.com/pricing",
|
|
756
|
+
)
|
|
757
|
+
redirect(session.url)
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Give exactly one of `plan` or `pack`, and `quantity` only with a pack. A pack is
|
|
761
|
+
named as the pack offers from [`billing.state`](#a-customers-billing) name it:
|
|
762
|
+
by its price's lookup key, or by the price id when it has none. Dodo Payments
|
|
763
|
+
has no lookup keys, so there it is the product id, which differs between test
|
|
764
|
+
and live mode: take it from the offer, chosen by
|
|
765
|
+
[what it grants](#a-customers-billing). A plan bound at more than one interval
|
|
766
|
+
sells its first bound price, unless `price` names another: a plan offer's
|
|
767
|
+
`price.id` or `price.lookup_key`. `checkout` reuses the provider's customer
|
|
768
|
+
already linked to the customer, and a customer Meterbase has not seen yet is
|
|
769
|
+
created when their purchase is applied. `success_url` is https, or http on
|
|
770
|
+
localhost; Meterbase adds `session_id`, its own opaque id for the checkout, to
|
|
771
|
+
it. The session expires at `session.expires_at`: an hour after it opens on
|
|
772
|
+
Stripe, and 24 hours on Dodo Payments.
|
|
773
|
+
|
|
774
|
+
A plan's checkout opens a new subscription, so it is for a customer without a
|
|
775
|
+
paying one. For a customer whose subscription pays and is not cancelling
|
|
776
|
+
(`billing.status` is `active`, `trialing` or `past_due` in their
|
|
777
|
+
[state](#a-customers-billing), and `cancel_at_period_end` is false), it raises
|
|
778
|
+
`ConflictError` with `code == "already_subscribed"`. There is no plan-change
|
|
779
|
+
call yet: a customer who already pays changes plan or interval in the provider's
|
|
780
|
+
customer portal, so link there for their `upgrade`, `downgrade` and
|
|
781
|
+
`switch_interval` offers. A customer whose subscription is cancelling can check
|
|
782
|
+
out a new one, which takes over when the old one ends; its purchase reads `held`
|
|
783
|
+
until then.
|
|
784
|
+
|
|
785
|
+
Set the portal up once per mode, on the provider's side, and give it only the
|
|
786
|
+
prices or products you bound: a subscription moved onto any other is held for
|
|
787
|
+
review under **Payments**.
|
|
788
|
+
|
|
789
|
+
- **Stripe:** under Settings → Billing → Customer portal, turn on **Switch
|
|
790
|
+
plan**, add the bound products and prices, and save. Then **Activate link**,
|
|
791
|
+
and link to the login link it gives (`https://billing.stripe.com/p/login/…`),
|
|
792
|
+
where the customer signs in with the email they paid with. In test mode,
|
|
793
|
+
open a customer's portal from their page in the Stripe Dashboard.
|
|
794
|
+
- **Dodo Payments:** put the bound subscription products in one Product
|
|
795
|
+
Collection, since the portal changes plans only within one, and turn on
|
|
796
|
+
**Allow Subscription Updates** under Settings → Subscriptions. Link to
|
|
797
|
+
`https://customer.dodopayments.com/login/{business_id}`, where the customer
|
|
798
|
+
asks for a sign-in link by the email they paid with. Test mode sends no
|
|
799
|
+
sign-in emails, so there your server makes a session with your Dodo key:
|
|
800
|
+
`POST /customers/{customer_id}/customer-portal/session`, with the
|
|
801
|
+
`customer.customer_id` of `GET /subscriptions/{id}`, for the
|
|
802
|
+
`billing.subscription_id` in their [state](#a-customers-billing).
|
|
803
|
+
|
|
804
|
+
A checkout is never retried unless it is given an `idempotency_key`, 1 to 200
|
|
805
|
+
printable ASCII characters with no spaces. Until the session expires, a repeat
|
|
806
|
+
with the same key answers that session rather than opening another when it
|
|
807
|
+
names the same customer, plan or pack, price and quantity, and raises
|
|
808
|
+
`IdempotencyKeyReusedError` (409 `idempotency_key_reused`) otherwise. The return
|
|
809
|
+
URLs stay the first request's. Once the session expires, the key opens a new
|
|
810
|
+
one. What each call can raise is under [Billing errors](#billing-errors).
|
|
811
|
+
|
|
812
|
+
### The return page
|
|
813
|
+
|
|
814
|
+
The provider can send the customer back before its webhook has arrived, so the
|
|
815
|
+
return page waits for the purchase to land. Dodo Payments adds query parameters
|
|
816
|
+
of its own, such as `status`; they prove nothing about payment, so read the
|
|
817
|
+
purchase by `session_id`:
|
|
818
|
+
|
|
819
|
+
```python
|
|
820
|
+
purchase = meterbase.purchases.wait(request.args["session_id"], timeout=15)
|
|
821
|
+
|
|
822
|
+
if purchase is None:
|
|
823
|
+
... # no checkout has that session_id
|
|
824
|
+
elif purchase.state in ("waiting", "settling"):
|
|
825
|
+
... # still on its way: show "Confirming your payment"
|
|
826
|
+
elif purchase.state in ("fulfilled", "late", "held"):
|
|
827
|
+
... # paid; only "fulfilled" means the plan or the pack is in place
|
|
828
|
+
else:
|
|
829
|
+
... # "failed" or "expired": nothing was bought
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
| `state` | Meaning |
|
|
833
|
+
| ----------- | ------------------------------------------------------------------------- |
|
|
834
|
+
| `waiting` | the checkout is open, or it was paid and is not applied yet |
|
|
835
|
+
| `settling` | paid by a method that clears later, such as a bank debit |
|
|
836
|
+
| `fulfilled` | the plan or the pack is in place |
|
|
837
|
+
| `late` | paid over 90 s ago and still not applied: the customer can close the page |
|
|
838
|
+
| `held` | paid, and held for review under **Payments** in the dashboard |
|
|
839
|
+
| `failed` | the payment failed |
|
|
840
|
+
| `expired` | the checkout closed unpaid |
|
|
841
|
+
|
|
842
|
+
`wait` reads the purchase every `interval` seconds, 2 unless set, for up to
|
|
843
|
+
`timeout` seconds, 90 unless set, and returns as soon as the state is anything
|
|
844
|
+
but `waiting` or `settling`. Running out of time is not an error: it returns
|
|
845
|
+
the last read, so show a purchase still pending as pending, and read it again
|
|
846
|
+
from the page. Many hosts cut a request off before 90 s, so a wait inside the
|
|
847
|
+
page's request passes a shorter `timeout`, as above. It returns `None` when the
|
|
848
|
+
purchase was never found, which means no checkout has that id, as with an
|
|
849
|
+
edited link. A 404 reads as waiting, in case the provider does not show a new
|
|
850
|
+
session yet. A `RateLimitError` reads as waiting too, with the next read after
|
|
851
|
+
its `retry_after` when that is longer than `interval`, since every return page
|
|
852
|
+
on one key shares the 120 reads per 10 s.
|
|
853
|
+
`timeout` bounds the polling, not each read, which keeps the client's own
|
|
854
|
+
timeout and retries. A negative or NaN `timeout`, an `interval` that is not
|
|
855
|
+
above 0, or an empty session id raises `MeterbaseError` and sends nothing.
|
|
856
|
+
`purchases.retrieve` reads it once.
|
|
857
|
+
|
|
858
|
+
### A customer's billing
|
|
859
|
+
|
|
860
|
+
```python
|
|
861
|
+
state = meterbase.billing.state("acct_1")
|
|
862
|
+
|
|
863
|
+
state.plan # BillingPlan(key="pro", name="Pro"), or None
|
|
864
|
+
if state.billing and state.billing.status == "past_due":
|
|
865
|
+
print("Payment failing, next retry:", state.billing.next_retry_at) # None on Dodo
|
|
866
|
+
for offer in state.offers:
|
|
867
|
+
print(offer.action, offer.plan or offer.pack, offer.price.unit_amount)
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
One read for a pricing or account page: the plan and its `period`, a change
|
|
871
|
+
waiting to land (`pending_change`), each meter the plan meters (`allowances`),
|
|
872
|
+
the open grants beside it, purchased packs included (`grants`), the provider's
|
|
873
|
+
subscription the plan follows (`billing`, `None` when there is none), and every
|
|
874
|
+
bound price on offer. A customer Meterbase has not seen yet reads as one with no
|
|
875
|
+
plan, every plan offer an `upgrade`, so a pricing page can show a new visitor
|
|
876
|
+
what to buy before they buy. A customer that was deleted raises `NotFoundError`
|
|
877
|
+
(404 `customer_not_found`).
|
|
878
|
+
|
|
879
|
+
`billing.status` reads the same on both providers, with a cancelled or expired
|
|
880
|
+
subscription `ended`. Dodo Payments has no `trialing`, since a trial reads
|
|
881
|
+
`active`, and its `on_hold` reads as `unpaid`; `next_retry_at` and `lookup_key`
|
|
882
|
+
are always `None` there.
|
|
883
|
+
|
|
884
|
+
An offer's `action` is `current`, `switch_interval` for the same plan billed
|
|
885
|
+
over another interval, `upgrade` or `downgrade` by price per month against the
|
|
886
|
+
current plan, or `purchase` for a pack. A plan offer is sold to a customer
|
|
887
|
+
without a paying subscription with `checkout(..., plan=offer.plan,
|
|
888
|
+
price=offer.price.id)`, and a pack offer with `pack=offer.pack`. New actions can
|
|
889
|
+
be added; treat one you do not know as unavailable. `price.unit_amount` is the
|
|
890
|
+
price as listed, in the currency's minor unit, cents for USD. Dodo Payments adds
|
|
891
|
+
tax at checkout, by the buyer's location, unless the product's price is tax
|
|
892
|
+
inclusive, so a Dodo Payments buyer can pay more than the offer shows.
|
|
893
|
+
|
|
894
|
+
A pack offer's `grant` is what one pack gives: `amount` on the meter whose key
|
|
895
|
+
is `meter`, twice that for `quantity=2`, expiring `expires_days` after the
|
|
896
|
+
payment, or never when that is `None`. A plan offer's `grant` is `None`. Choose
|
|
897
|
+
a pack by its grant rather than by name, telling two packs on one meter apart
|
|
898
|
+
by `amount`: Dodo Payments has no lookup keys, and its product ids differ
|
|
899
|
+
between test and live mode.
|
|
900
|
+
|
|
901
|
+
```python
|
|
902
|
+
top_up = next(
|
|
903
|
+
(o for o in state.offers if o.grant and o.grant.meter == "ai_tokens"), None
|
|
904
|
+
)
|
|
905
|
+
# top_up.grant == {"meter": "ai_tokens", "amount": 200_000, "expires_days": 90}
|
|
906
|
+
|
|
907
|
+
if top_up and top_up.pack:
|
|
908
|
+
session = meterbase.billing.checkout(
|
|
909
|
+
"acct_1",
|
|
910
|
+
pack=top_up.pack,
|
|
911
|
+
success_url="https://app.example.com/billing/done",
|
|
912
|
+
)
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### Billing errors
|
|
916
|
+
|
|
917
|
+
| `code` | Raises | Calls | When |
|
|
918
|
+
| ------------------------ | -------------------------------- | ------------------- | ------------------------------------------------------------------------ |
|
|
919
|
+
| `unauthenticated` | `AuthenticationError`, 401 | all | unknown, revoked or malformed key |
|
|
920
|
+
| `payments_not_connected` | `ConflictError`, 409 | all | the workspace has no payment provider connected |
|
|
921
|
+
| `already_subscribed` | `ConflictError`, 409 | `checkout` | a plan checkout for a customer who already pays |
|
|
922
|
+
| `customer_deleted` | `ConflictError`, 409 | `checkout` | the customer was deleted |
|
|
923
|
+
| `idempotency_key_reused` | `IdempotencyKeyReusedError`, 409 | `checkout` | the `idempotency_key` was used for a different checkout |
|
|
924
|
+
| `customer_not_found` | `NotFoundError`, 404 | `state` | the customer was deleted |
|
|
925
|
+
| `purchase_not_found` | `NotFoundError`, 404 | `purchases` | no checkout has that session id; `wait` reads it as waiting |
|
|
926
|
+
| `price_not_bound` | `InvalidRequestError`, 422 | `checkout` | the plan or pack has nothing bound, or `price` is not bound to the plan |
|
|
927
|
+
| `invalid_request` | `InvalidRequestError`, 422 | `checkout`, `state` | a field failed validation; the message names it |
|
|
928
|
+
| `rate_limited` | `RateLimitError`, 429 | all | past a [limit](#billing), with `retry_after`; `wait` keeps waiting |
|
|
929
|
+
| `provider_error` | `ServerError`, 502 | all | the provider refused or failed; the message says which |
|
|
930
|
+
| `unavailable` | `ServerError`, 503 | all | the connected key could not be opened: disconnect, then connect it again |
|
|
931
|
+
| `engine_unavailable` | `ServerError`, 503 | all | Meterbase could not be reached |
|
|
932
|
+
|
|
726
933
|
## Webhooks
|
|
727
934
|
|
|
728
935
|
Meterbase posts to your endpoints when a customer crosses a usage threshold,
|
|
@@ -893,13 +1100,13 @@ except RateLimitError as error:
|
|
|
893
1100
|
| `NotFoundError` | 404 |
|
|
894
1101
|
| `ConflictError` | 409 — a key or external id is taken |
|
|
895
1102
|
| `PeriodSettlingError` | 409 — a `ConflictError`: the period ended under a minute ago, so read it again after `retry_after` seconds |
|
|
896
|
-
| `IdempotencyKeyReusedError` | 409 — a `ConflictError`: the `idempotency_key` on a grant
|
|
1103
|
+
| `IdempotencyKeyReusedError` | 409 — a `ConflictError`: the `idempotency_key` on a grant, a plan change or a `billing.checkout` was already used for a different request. No grant or plan change was written, and no checkout opened |
|
|
897
1104
|
| `InvalidRequestError` | 422 — a field failed validation; or `400 invalid_query`, a query parameter that did |
|
|
898
1105
|
| `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart` |
|
|
899
1106
|
| `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7 |
|
|
900
1107
|
| `TooLateError` | 422 — an `InvalidRequestError`: the key is over an hour from the engine's clock, which it carries on `server_time`. Nothing was recorded |
|
|
901
1108
|
| `ReservationMismatchError` | 422 — an `InvalidRequestError`: the reservation was made for a different customer or meter |
|
|
902
|
-
| `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 |
|
|
1109
|
+
| `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 |
|
|
903
1110
|
| `ServerError` | 5xx |
|
|
904
1111
|
| `APIConnectionError` / `APITimeoutError` | the request never got an answer |
|
|
905
1112
|
| `WebhookVerificationError` | `verify_webhook`: not a delivery Meterbase signed in the last five minutes |
|
|
@@ -918,7 +1125,8 @@ SDK would raise for a response, for realistic errors in your own tests.
|
|
|
918
1125
|
```python
|
|
919
1126
|
Meterbase(
|
|
920
1127
|
api_key="mb_sk_live_…",
|
|
921
|
-
base_url="https://api.meterbase.
|
|
1128
|
+
base_url="https://api.meterbase.tech",
|
|
1129
|
+
billing_base_url="https://service.meterbase.tech", # billing and purchases
|
|
922
1130
|
timeout=10.0, # seconds, per attempt, on every call; unset, 35, and 3 for check
|
|
923
1131
|
max_retries=2,
|
|
924
1132
|
http_client=httpx.Client(proxy="http://localhost:8030"), # yours to close
|
|
@@ -932,7 +1140,9 @@ milliseconds when the engine is well. Decide what an `APITimeoutError` means
|
|
|
932
1140
|
there — failing open or failing closed — rather than leaving the user waiting
|
|
933
1141
|
out the 35 s the writes get. Pass an `http_client` for proxies,
|
|
934
1142
|
limits or a transport of your own: an `httpx.Client` for `Meterbase`, an
|
|
935
|
-
`httpx.AsyncClient` for `AsyncMeterbase`.
|
|
1143
|
+
`httpx.AsyncClient` for `AsyncMeterbase`. `billing` and `purchases` go to
|
|
1144
|
+
`billing_base_url`, `https://service.meterbase.tech` unless set, and every other
|
|
1145
|
+
call to `base_url`, over the same connections.
|
|
936
1146
|
|
|
937
1147
|
For other settings on some calls, derive a client with them. It shares the
|
|
938
1148
|
original's connections, and its clock:
|
|
@@ -951,14 +1161,15 @@ cancelled `track` cannot record usage twice. A sync call cannot be cancelled;
|
|
|
951
1161
|
bound it with `timeout` and `max_retries` instead.
|
|
952
1162
|
|
|
953
1163
|
**Retries** apply to `GET`, `DELETE`, `track`, `reserve`, `customers.ensure`,
|
|
954
|
-
and a grant
|
|
955
|
-
timeouts, 408, 429 and 5xx, with exponential backoff and
|
|
956
|
-
`Retry-After` is honoured when
|
|
957
|
-
other `POST` and `PATCH` is never replayed, because a retried
|
|
958
|
-
produce a second row. An `ensure` can be, because running it twice
|
|
959
|
-
nothing: one whose first attempt landed answers `created` false. So can
|
|
960
|
-
grant or plan change, which the engine answers with what the first
|
|
961
|
-
wrote: one whose first attempt landed answers `replayed` true.
|
|
1164
|
+
and a grant, plan change or `billing.checkout` given an `idempotency_key`, on
|
|
1165
|
+
connection failures, timeouts, 408, 429 and 5xx, with exponential backoff and
|
|
1166
|
+
full jitter. `Retry-After` is honoured when Meterbase sends it, up to eight
|
|
1167
|
+
seconds. Every other `POST` and `PATCH` is never replayed, because a retried
|
|
1168
|
+
create could produce a second row. An `ensure` can be, because running it twice
|
|
1169
|
+
changes nothing: one whose first attempt landed answers `created` false. So can
|
|
1170
|
+
a keyed grant or plan change, which the engine answers with what the first
|
|
1171
|
+
attempt wrote: one whose first attempt landed answers `replayed` true. A keyed
|
|
1172
|
+
checkout is answered by Meterbase with the session its first attempt opened.
|
|
962
1173
|
|
|
963
1174
|
`track` and `reserve` are the exception because they carry an idempotency key,
|
|
964
1175
|
and because the SDK fixes that key **before** the retry loop starts: every
|
|
@@ -987,6 +1198,12 @@ Pre-1.0, the minor version is where breaking changes land — deliberately, so a
|
|
|
987
1198
|
change that will not run against your code cannot arrive as a patch. The SDK
|
|
988
1199
|
follows the engine's API, as the TypeScript SDK (`mbase-sdk`) does.
|
|
989
1200
|
|
|
1201
|
+
**0.5.0** adds `billing` and `purchases`, for a workspace that sells through
|
|
1202
|
+
Stripe or Dodo Payments: `billing.checkout`, `billing.state`,
|
|
1203
|
+
`purchases.retrieve` and `purchases.wait`, with the `billing_base_url` option.
|
|
1204
|
+
They go to the service at `https://service.meterbase.tech` rather than the
|
|
1205
|
+
engine. Nothing in it breaks code written for 0.4.0.
|
|
1206
|
+
|
|
990
1207
|
**0.4.0** pages the customer list, and adds `customers.list_all` and the
|
|
991
1208
|
`limit`, `starting_after`, `plan` and `has_plan` parameters. One change can break
|
|
992
1209
|
code: `customers.list()` answers one page of 100, newest first, where it used to
|