Subscription System
Subscription System pattern for Python.
An end-to-end recipe for a SaaS subscription system: catalog setup, sign-up, mid-cycle upgrades with proration, pause, and cancel-at-period- end. All using the Python SDK.
Goal
You're building a SaaS app with monthly + annual plans, optional add-on seats, and a self-service "Manage subscription" page.
The data model:
- Products — one per plan tier ("Free", "Pro", "Team").
- Prices — recurring monthly / annual price per tier, plus a per-seat add-on price.
- Subscriptions — one per customer; items reference prices.
Implementation
1. Bootstrap the catalog (one-time)
import os
from easylabs import Client
client = Client(api_key=os.environ["EASY_API_KEY"])
pro = client.products.create(name="Pro", description="Pro tier")
team = client.products.create(name="Team", description="Team tier")
pro_monthly = client.product_prices.create(
product_id=pro.id,
currency="USD",
unit_amount=2900,
recurring=True, interval="month", interval_count=1,
)
pro_annual = client.product_prices.create(
product_id=pro.id,
currency="USD",
unit_amount=29000,
recurring=True, interval="year", interval_count=1,
)
seat_addon = client.product_prices.create(
product_id=team.id,
currency="USD",
unit_amount=1500,
recurring=True, interval="month", interval_count=1,
)2. Sign-up flow — checkout + start subscription atomically
def signup(*, user, plan_price_id, card_token):
return client.checkout.create(
customer={
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
},
payment_instrument={
"type": "PAYMENT_CARD",
"name": user.full_name,
"token_id": card_token,
},
subscriptions=[{"items": [{"price_id": plan_price_id, "quantity": 1}]}],
idempotency_key=f"signup-{user.id}",
)Persist result.subscriptions[0]["id"] against the user record so you
can manage the subscription later.
3. Mid-cycle plan change with proration preview
def preview_upgrade(*, sub_id, new_price_id, old_item_id):
return client.subscriptions.proration_preview(
sub_id,
items=[{"price_id": new_price_id, "quantity": 1}],
remove_items=[old_item_id],
)
def apply_upgrade(*, sub_id, new_price_id, old_item_id):
client.subscriptions.add_item(
sub_id, price_id=new_price_id,
idempotency_key=f"upgrade-{sub_id}-add-{new_price_id}",
)
client.subscriptions.remove_item(
sub_id, old_item_id,
idempotency_key=f"upgrade-{sub_id}-remove-{old_item_id}",
)4. Add seats to a Team subscription
def set_seat_count(*, sub_id, seat_item_id, seats):
client.subscriptions.update_item(
sub_id, seat_item_id,
quantity=seats,
idempotency_key=f"seats-{sub_id}-{seats}",
)5. Apply a coupon
def apply_coupon(*, sub_id, coupon_id):
client.subscriptions.apply_discount(
sub_id,
coupon_id=coupon_id,
idempotency_key=f"discount-{sub_id}-{coupon_id}",
)6. Pause / resume / cancel
def pause(sub_id, resumes_at):
client.subscriptions.pause(
sub_id, behavior="mark_uncollectible", resumes_at=resumes_at,
)
def resume(sub_id):
client.subscriptions.resume(sub_id)
def cancel(sub_id, immediate=False):
client.subscriptions.cancel(
sub_id,
at_period_end=not immediate,
)7. React to lifecycle webhooks
def on_event(event):
if event.type == "subscription.trial_will_end":
send_trial_ending_email(event.data["identity_id"])
elif event.type == "invoice.payment_failed":
notify_dunning(event.data["customer_id"], event.data["id"])
elif event.type == "subscription.deleted":
downgrade_account(event.data["identity_id"])Tradeoffs
- Always preview proration before applying. Show the user the exact amount they'll be charged today; surprise charges drive disputes.
- Use
cancel(at_period_end=True)for self-service cancellations. It avoids refund accounting, leaves the customer with paid access until the cycle ends, and gives you a window to send a save-attempt email. - Configure dunning before going live. See the Dunning page — without it, failed renewals silently retry once and then cancel.
- One subscription per customer is simplest. Multiple parallel subscriptions per customer are supported but make the UI and invoicing materially harder.