Subscriptions
A subscription charges a card the customer has already saved, on a plan’s own calendar. It goes through exactly the same path as any other off-session charge, so mandates, receipts, the ledger and authentication recovery all apply — what subscriptions add is when.
A plan is what you sell on repeat: a name, a price, and how often it repeats.
A plan’s price never changes. Charging something different means a new plan. That is on purpose: a subscription can always say what it agreed to, and an old invoice still explains itself. Renaming and metadata are all an update may touch.
import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
monthly = tuppence.plans.create(name="Box of the month", amount=2500, interval="month")yearly = tuppence.plans.create(name="Box of the year", amount=25000, interval="year")
print(monthly.amount, monthly.interval) # 2500 monthprint(yearly.status) # active
# Renaming is fine. The price is not a field you can send.renamed = tuppence.plans.update(monthly.id, name="Box of the month (large)")print(renamed.name, renamed.amount) # Box of the month (large) 2500Archiving stops new subscriptions and leaves the ones already on it alone:
import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
old = tuppence.plans.create(name="Retired tier", amount=900, interval="month")tuppence.plans.archive(old.id)print([p.name for p in tuppence.plans.list(status="active").data][:2])Starting one
Section titled “Starting one”A subscription needs a card the customer saved — take one payment with
save_card="off_session" first, and the payer agrees to the terms when they pay it.
The first period is charged straight away, unless the plan has a trial.
subscription = tuppence.subscriptions.create(customer=customer.id, plan=monthly.id)print(subscription.status, subscription.current_period_end) # active …By default it charges the customer’s most recent saved card; name another with
payment_method.
Changing plan
Section titled “Changing plan”The period never moves. Changing plan is not restarting: a customer who changes twice in a month is not billed three times for it, and they renew on the day they always renewed on.
preview prices the change — the unused time on the old plan credited, the remaining time on
the new one charged — and hands back the instant it priced at. Pass that proration_at back to
change and the charge is the number you were shown, to the penny.
quote = tuppence.subscriptions.preview(subscription.id, plan=yearly.id)for line in quote.lines: print(line.description, line.amount)print(quote.amount, quote.at_period_end)
tuppence.subscriptions.change(subscription.id, plan=yearly.id, proration_at=quote.proration_at)Dearer now, cheaper later, because that is the honest way round:
- An upgrade is charged for the time remaining and takes effect once paid — nobody is moved onto a plan they have not paid for.
- A cheaper plan waits for the period they have already paid for.
at_period_enddefaults totruefor a downgrade andfalsefor an upgrade. - Asked for immediately, a downgrade hands back the unused time as credit against the next renewal, not as a refund to a card that was never charged for that time. Credit larger than the next renewal buys a whole period with no card touched at all.
When a card stops working
Section titled “When a card stops working”A failed renewal does not cancel anything. The customer agreed to pay, so the subscription goes
past_due and we try again after 1, 3, 7 and 14 days, telling the payer each time — with
what is owed, when we will try again, and a link where they can pay with another card. Paying
that link collects the money and replaces the card we keep, and the subscription carries on.
If the card never works, it stops after the last try, and the payer is told that too.
You hear about all of it:
| Event | When |
|---|---|
subscription.created |
It exists |
subscription.started |
The first period is paid |
subscription.renewed |
A period was charged |
subscription.updated |
The plan changed, or one is pending |
subscription.payment_failed |
An attempt failed; attempts says how many |
subscription.recovered |
They paid with another card |
subscription.canceled |
Ended — by you, or by the schedule running out |
Ending one
Section titled “Ending one”tuppence.subscriptions.cancel(subscription.id) # at the period endtuppence.subscriptions.cancel(subscription.id, at_period_end=False) # nowCancelling at the period end is the kind default: they keep what they have already paid for and are not charged again. Nothing already paid is refunded either way.
Testing a year in a second
Section titled “Testing a year in a second”Subscriptions renew when you advance a test clock — so a month of billing is a test, not a wait:
tuppence.request("POST", "/v1/test_helpers/clock/advance", body={"days": 32})print(tuppence.subscriptions.retrieve(subscription.id).current_period_start)See Testing for the rest of the clock.
Subscribing from a link
Section titled “Subscribing from a link”A payment link with mode="subscription" starts a subscription when it
is paid: the payer gives an email address, pays the first period, and their card is saved — no
account, no password, no dashboard. The plan decides the price, and the recurring terms on the
page are exactly what we keep as their mandate.
import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
plan = tuppence.plans.create(name="Coffee club", amount=1200, interval="month")link = tuppence.payment_links.create(name="Join the coffee club", mode="subscription", plan=plan.id)print(link.mode, link.url)A plan with a free trial cannot be sold from a link yet — the card is saved by paying the
first period, and a trial has none to pay. Start those with subscriptions.create.
What we do not do
Section titled “What we do not do”No usage-based billing, no invoices as separate objects, no coupons, and no customer portal yet. Tax is not calculated anywhere: see Not supported.