The
subscriptions resource exists in the Python SDK only. The JavaScript SDK has no subscriptions resource and no subscription methods. Wherever a checkout or portal link is needed from code, use the Python SDK or call the REST endpoints directly. See Subscriptions are Python-SDK-only below and the SDK parity matrix.Who can manage subscriptions
Every/subscriptions/* endpoint is organization-scoped and reserved for owners and admins:
- Most endpoints require admin or owner (the
organization_admin_requireddependency). POST /subscriptions/wallet/topuprequires owner specifically.
member role is retired: only owners and admins exist. Every request must carry the organization context header. See Org context & X-Organization-ID and Roles & permissions.
string
required
A bearer token: either a
mx_live_… API key or a Clerk session token. Example: Authorization: Bearer mx_live_…. See Authentication.string
required
The UUID of the organization whose subscription you are managing. Missing this header on an org-scoped route returns
400/422.These endpoints have no global
/v1 prefix — each router carries its own. The subscription endpoints live under /subscriptions, and the Stripe webhook lives under /stripe. Trailing-slash redirects are off, so call paths exactly as written. See Base URLs & versioning.Lifecycle states
A subscription’sstatus mirrors Stripe verbatim. ModuleX treats the following set as active (entitlements apply): trialing, active, past_due, unpaid. Anything outside that set falls the organization back to the Free plan.
operations_suspended is the flag that gates a paying-but-unpaid organization. It is set when status is unpaid, or when status is past_due and the failed-payment grace window (anchored on the first failure, plus 3 days) has passed. A suspended organization’s managed runs return a credit_exhausted 402 from the gate and do not fall back to the Free tier. Restoring payment clears the flag — see Trials & dunning.Creating a subscription — Stripe Checkout
A new subscription starts with a Stripe Checkout session in subscription mode. ModuleX returns a hosted checkout URL; you redirect the owner or admin to it, and the subscription is created by Stripe and mirrored back through the webhook.POST /subscriptions/checkout-link
Creates a Stripe Checkout session (mode=subscription) for a plan and billing interval. Requires admin or owner.
string
required
The billing interval:
month or year.string
The plan to subscribe to:
pro or max. Preferred over plan_id. One of plan_slug or plan_id is required.string
deprecated
A legacy database plan id. Deprecated — use
plan_slug.string
The hosted Stripe Checkout URL. Redirect the user here to complete payment. On completion, Stripe redirects to
{FRONTEND_URL}/settings/billing?checkout=success; on abandonment, to ?checkout=canceled.The Stripe price is resolved live by lookup key (for example
pro_monthly, max_annual), never a hard-coded id. The checkout quantity is the organization’s active member count (the owner is seat 1). The session stamps subscription_data.metadata = { organization_id, plan_slug } so the webhook can attribute the resulting subscription.Errors for POST /subscriptions/checkout-link
Errors for POST /subscriptions/checkout-link
These are plain
{"detail": "…"} (HTTPException) shapes, not the billing DenialEnvelope. See Errors & status codes.Trials
A 7-day trial is added to the checkout automatically when both conditions hold:- The organization is trial-eligible: it has never trialed and the owner has never subscribed before. ModuleX tracks this with
organizations.has_ever_trialedandusers.has_ever_subscribed. - The synced Stripe price has
trial_period_days > 0.
trial_settings.end_behavior.missing_payment_method = "cancel"). Full eligibility and grace behavior is on Trials & dunning.
Changing plans — transitions
Once a subscription exists, an admin or owner changes plan or interval through a transition. ModuleX computes the direction (upgrade, downgrade, or no-op) and applies it either immediately or at the period end.POST /subscriptions/transition
Self-serve plan change. Requires admin or owner. Seats are preserved across the change (the quantity is carried over).
string
required
The target plan:
pro or max. Enterprise transitions are rejected — Enterprise is sales-led.string
default:"month"
The target billing interval:
month or year.string
One of
upgrade, downgrade, or noop.string
immediate for upgrades, period_end for downgrades. Absent on a no-op.string
ISO 8601 timestamp when a
period_end change takes effect (downgrades only).string
The plan the downgrade will move to (downgrades only).
string
The interval the downgrade will move to (downgrades only).
string
The Stripe
SubscriptionSchedule id backing a scheduled downgrade (downgrades only).string
Present on a no-op:
"Already on this plan + interval."Upgrades modify the Stripe subscription immediately with
proration_behavior="always_invoice" and payment_behavior="error_if_incomplete", under a per-organization advisory lock. If the card is declined, the change is rejected with a 402 upgrade_payment_failed DenialEnvelope (see below). After an upgrade, the entitlement cache is busted so the new plan takes effect at once.Downgrades (and annual → monthly) are applied as a two-phase Stripe SubscriptionSchedule. The organization keeps its current, higher entitlements until the period-end webhook fires — the entitlement cache is not busted on a downgrade. The pending change is mirrored in pending_plan_slug, pending_interval, and pending_effective_at.Errors for POST /subscriptions/transition
Errors for POST /subscriptions/transition
POST /subscriptions/transition/cancel
Undoes a pending downgrade or a pending cancellation — whether it was scheduled from the app or from the Stripe customer portal. Requires admin or owner. It is idempotent: when nothing is pending it returns a no-op without error.
ModuleX detects a pending state from pending_plan_slug (a scheduled downgrade) or from cancel_at_period_end / cancel_at / stripe_schedule_id (a scheduled cancel), releases the Stripe schedule and/or resumes the subscription, and clears the pending mirror columns. Because the current tier is never lost in this operation, the entitlement cache is not busted.
boolean
true when a pending change was undone; false on a no-op.string
Present on a no-op explaining that nothing was pending.
cURL
The customer portal
The Stripe Billing Portal is where owners and admins update payment methods, view invoices, and cancel a subscription. ModuleX returns a one-time portal URL.POST /subscriptions/customer-portal
Creates a Stripe Billing Portal session. Requires admin or owner. Returns to {FRONTEND_URL}/?org_settings=settings/billing when the user is done.
string
The hosted Stripe customer portal URL. Redirect the user here.
A cancellation made in the Stripe portal arrives back in ModuleX as a scheduled period-end cancel. You can undo it with
POST /subscriptions/transition/cancel while it is still pending.Errors for POST /subscriptions/customer-portal
Errors for POST /subscriptions/customer-portal
Reading subscription state
Two read endpoints expose the current plan and the catalog. Both are the read methods the Python SDK wraps.GET /subscriptions/organization-billing
Returns the organization’s current subscription, its plan, and any scheduled change or cancellation. Requires admin or owner.
boolean
false when the organization has no active subscription (on Free). Then subscription and plan are null.object
The active subscription, or
null.object
The plan backing the subscription, or
null.Example response (active subscription)
Example response (active subscription)
GET /subscriptions/organization-plans
Lists the available plans with a per-organization relation (whether each plan is the current one, an upgrade, or a downgrade), whether it is selectable, and any discounted price. Requires admin or owner. The Admin plan is never exposed, and no Stripe ids are leaked.
array
The plan catalog.
integer
The number of plans returned.
Staying in sync — the Stripe webhook
ModuleX does not poll Stripe for state. Stripe pushes events to a single webhook, and ModuleX mirrors the relevant ones into its own subscription, wallet, and catalog tables. The webhook is the source of every status change you observe through the read endpoints.POST /stripe/webhook
The webhook receiver. It is not authenticated with a bearer token — it verifies a Stripe signature instead. It is idempotent, order-aware, and retryable.
string
required
The Stripe signature header. ModuleX verifies it against
STRIPE_WEBHOOK_SECRET using the raw request body. Verification or parse failure returns 400 "Invalid webhook signature" with no error detail leaked. A missing STRIPE_WEBHOOK_SECRET returns 500.This endpoint is a Stripe-to-server receiver, not a client call. Neither SDK exposes it, and you should never call it yourself — you configure its URL in your Stripe dashboard. See the SDK parity matrix for the full list of routes with no SDK method.
Handled events
Each delivered event is normalized once and dispatched by type. A redelivered event that was already processed short-circuits to{ "received": true, "duplicate": true }.
A
customer.subscription.deleted event mirrors the subscription’s terminal status; the organization then falls to Free because the deleted status is outside the active set. A restored payment after dunning clears suspension but does not re-grant credits or reset the monthly bucket — this is deliberate, to prevent farming a fresh allowance by lapsing and recovering. See Trials & dunning and Credits & metering.Retries and dead-lettering
The webhook answers Stripe so that genuinely transient failures are retried and permanent ones are not:1
Transient failure under the cap
An organization-not-found race (the customer-create event arrives before the org commit), or a database or Stripe error mid-processing, returns 503 so Stripe retries — up to
WEBHOOK_TRANSIENT_RETRY_CAP (default 5) attempts.2
Transient failure at the cap
Once the retry cap is reached, the event is dead-lettered, marked processed, and acknowledged with 200 so Stripe stops retrying:
{ "received": true, "dead_lettered": true, … }.3
Permanent failure
A malformed payload (for example a missing customer) raises a permanent error: the event is dead-lettered, marked processed, and acknowledged with 200. No retry.
4
Success
The event is recorded for deduplication and acknowledged with 200:
{ "received": true, "type": … }.Ordering. Subscription mirror upserts carry a watermark on the event’s
created timestamp. A stale (older) event that arrives after a newer one is rejected without corrupting the newer state, so out-of-order delivery from Stripe cannot roll your subscription back.Subscriptions are Python-SDK-only
Thesubscriptions resource is part of the Python SDK only. The JavaScript SDK has no subscriptions resource and exposes no subscription methods — confirmed against the SDK parity matrix. From JavaScript, call the REST endpoints directly (as shown in the JavaScript tabs above) or surface billing through the app’s customer portal.
The Python SDK wraps four of the subscription endpoints:
Every method accepts a per-call
organization_id that overrides the client default. All four methods are async and require an owner or admin token. The full method-to-route mapping, including the cross-SDK gaps, is on the SDK parity matrix; installation and configuration are on the Python SDK page.
Error shapes you may see
Subscription endpoints can return two different402 shapes, and you must branch on both:
- The flat
DenialEnvelope({ code, layer, key, current, limit, reason }) comes from the billing gate — for example an upgrade whose card was declined returnscode: "upgrade_payment_failed". This is the same envelope the usage gate raises for402/403/429. - The
HTTPExceptionshape ({"detail": {…}}) comes from the wallet paid-plan checks (PATCH /subscriptions/wallet/extra-usage,POST /subscriptions/wallet/topup), which return{"detail": {"reason": "paid_subscription_required"}}when an organization without a paid subscription tries to use a paid-only feature.
400, 404, 500, 502) use the plain {"detail": "…"} string shape. All three envelope shapes are catalogued on Errors & status codes.
In the Python SDK, a
402 raises PaymentRequiredError and, when the body carries a recognized billing layer, a more specific subclass (CreditExhaustedError, WalletError, or QuotaExceededError). A 403 raises PermissionError and a 429 raises RateLimitError. See Errors & retries.Configuration reference
These environment variables govern the Stripe integration on the server. They are listed here so you understand the behavior; you set them on the deployment, not through the API.Related pages
Plans & pricing
Free, Pro, Max, and Enterprise — allowances, limits, and both annual prices.
Wallet & top-ups
The prepaid wallet, the top-up checkout, and auto top-up.
Trials & dunning
Trial eligibility, grace periods, suspension, and recovery.
Usage gating & limits
The admission gate and its 402, 403, and 429 responses.
Python SDK
Install and configure the async client that exposes the subscriptions resource.
SDK parity matrix
Which routes each SDK covers — and where the gaps are.