Easy Labs
SDKsPythonExamples

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.

On this page