Skip to content

Subscriptions

A subscription charges a card the customer already saved, on a plan’s calendar. Each charge is an ordinary off-session charge, so mandates, receipts and 3-D Secure recovery all apply. A subscription only decides when.

  1. PlanPOST /v1/plans
  2. Save a cardsave_card: "off_session"
  3. SubscribePOST /v1/subscriptions
  4. Renewsan invoice each period

A plan is what you sell on repeat: a name, an amount, an interval (day, week, month, year), optionally interval_count (3 = quarterly) and trial_days.

A plan’s price never changes. A new price is a new plan, so a subscription always shows what it agreed to. An update can only change the name or metadata. Archiving stops new subscriptions; existing ones carry on.

Terminal window
curl -sS https://api.tuppence.ai/v1/plans \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "Box of the month", "amount": 2500, "interval": "month"}'

First save the customer’s card: take one payment with customer and save_card: "off_session". The payer agrees to the terms when they pay it. Or let a link do both.

The first period is charged at once unless the plan has a trial. The subscription charges the customer’s most recent saved card; name another with payment_method.

Terminal window
curl -sS https://api.tuppence.ai/v1/subscriptions \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"customer": "cus_…", "plan": "plan_…"}'
trialingactivepast_dueretries 1, 3, 7, 14 dayspausedcanceledtrial endsrenewal failedrecoveredpauseresumeretries run outcancel
EventWhen
subscription.createdIt exists
subscription.startedThe first period is paid
subscription.renewedA period was charged
subscription.updatedThe plan changed, or a change is pending
subscription.payment_failedAn attempt failed; attempts says how many, failure_code why (null when unknown)
subscription.recoveredThey paid with another card
subscription.canceledEnded: by you, or by the schedule running out
subscription.pausedPaused; resumes_at says until when, or null for until resumed
subscription.resumedRunning again
subscription.trial_will_endThree days before a trial becomes a charge; trial_end says when
invoice.created, invoice.paid, invoice.payment_failedEach invoice

The period never moves. A customer who changes twice in a month isn’t billed three times and still renews on the same day.

  1. preview prices the change: unused time on the old plan is credited, remaining time on the new one charged. It returns the instant it priced at, proration_at.
  2. Pass proration_at to change and the charge matches the preview, to the penny.
Terminal window
curl -sS https://api.tuppence.ai/v1/subscriptions/sub_…/preview \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{"plan": "plan_…"}'
curl -sS https://api.tuppence.ai/v1/subscriptions/sub_…/change \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{"plan": "plan_…", "proration_at": 1798761600}'
ChangeWhat happens
UpgradeCharged for the time remaining; takes effect once paid. at_period_end defaults to false
DowngradeWaits for the period already paid for. at_period_end defaults to true
Downgrade, nowUnused time becomes credit against the next renewal, not a refund. Credit larger than a renewal buys a whole period with no card touched

A failed renewal cancels nothing. The subscription goes past_due and we retry after 1, 3, 7 and 14 days, emailing the payer each time with the amount owed, the next try, and a link to pay with another card. Paying through the link collects the money and replaces the saved card. After the last failed try the subscription ends and the payer is told.

If your UK business takes Direct Debit, a subscription can be paid from a saved bank account (Bacs, in pounds). A subscription link offers it beside cards. The first debit saves the account, and the subscription starts when that debit succeeds. A debit takes about three working days:

CaseWhat happens
A renewalIts invoice is processing. The period moves on, once, when the debit succeeds and the invoice is paid
A debit failsThe invoice opens again and the retries start. Each retry is a new debit with a new advance notice. Only insufficient_funds and generic_could_not_process are retried. For other failures the payer is asked to pay another way by the end of the window
The Direct Debit is cancelledNothing more is debited. At the next renewal the invoice stays open, the subscription is past_due, the payer is emailed a link to pay (by card or a new Direct Debit), and you get subscription.payment_failed with failure_code: "payment_method_unusable" and an alert. Paying that link saves the new way to pay
Money lands after it endedThe subscription stays ended. The invoice is marked paid and you get an alert to refund or restart. Only a payment begun after it ended, through the last email’s link, restarts it
Terminal window
curl -sS https://api.tuppence.ai/v1/subscriptions/sub_…/cancel \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{"at_period_end": true}'

Cancelling at the period end is usually kinder: the customer keeps what they paid for and isn’t charged again. Nothing already paid is refunded either way. Call reactivate before the period ends to make it renew as before.

Pausing stops the next charge; the paid period runs to its end. Pause until a date (resumes_at) or until you resume.

Terminal window
curl -sS https://api.tuppence.ai/v1/subscriptions/sub_…/pause \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{"resumes_at": 1798761600}'
ResumedWhat happens
Inside the paid periodIt carries on and renews when that period ends
After itA new period starts that day and is charged then; a card that stopped working meanwhile goes through the usual retries
Can’t pauseA trial (it’s free already), or a subscription that owes a payment

A coupon takes a percentage (percent_off) or an amount (amount_off, with currency) off a subscription’s charges. Its duration is once, repeating (with duration_in_periods) or forever. Its terms never change. A promo code is a name a customer types for a coupon, matched case-insensitively.

Terminal window
curl -sS https://api.tuppence.ai/v1/coupons \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"percent_off": 50, "duration": "repeating", "duration_in_periods": 3}'
curl -sS https://api.tuppence.ai/v1/promo_codes \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"coupon": "cpn_…", "code": "SPRING", "max_redemptions": 100, "first_time_only": true}'
Rule
RoundingA percentage rounds down, so a coupon never gives more than it says. If a discount leaves less than a card will take, that charge is free
Limitsmax_redemptions (on a coupon and a code), redeem_by and expires_at are enforced when the subscription is created, so the last redemption goes to exactly one customer
first_time_onlyOnly for a customer who has never subscribed on your account
StoppingArchive a coupon or deactivate a code to stop new redemptions. Subscriptions that already have it keep it

Every charge has an invoice, opened before the card is charged: one per period (and per mid-period change), however many retries. Invoices are numbered 1, 2, 3… per account without gaps. The lines (price, coupon, credit) add up to exactly what the card was asked for.

Terminal window
curl -sS "https://api.tuppence.ai/v1/invoices?subscription=sub_…" \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY"
statusMeans
openOwed, and being retried
processingA Direct Debit for it is on its way
paidA payment settled it
uncollectibleThe retries ran out

GET /v1/invoices/{id}/pdf returns the PDF; hosted_invoice_url is the payer’s copy behind an unguessable link. We do not calculate tax. An invoice shows the price and what came off it. Include VAT or sales tax in your prices and your own records.

To let payers manage their own subscription, create a portal session for one customer and send them to its url. It works for an hour, for that customer only.

Terminal window
curl -sS https://api.tuppence.ai/v1/portal_sessions \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"customer": "cus_…", "return_url": "https://example.com/account", "allowed_actions": ["cancel", "update_card", "invoices"]}'

The payer sees their subscriptions and invoices and can do what you allowed:

  • cancel at the end of the paid period (and undo it)
  • change to one of the plans you offer, at the previewed price
  • add a new card (charged at once if a payment is behind)
  • download invoices

Each subscription shows how it’s paid: card brand and last digits, or Bacs Direct Debit with the account’s last digits and mandate reference (and, if cancelled, an offer to update). A new way to pay added in the portal is always a card. Create a new session each time a signed-in customer asks.

Three days before, the payer is emailed the amount and date of the charge, and you get subscription.trial_will_end. When the trial ends, the first period is charged and starts that day. It renews on that day of the month from then on.

Subscriptions renew when you advance a test clock:

Terminal window
curl -sS https://api.tuppence.ai/v1/test_helpers/clock/advance \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \
-H "Content-Type: application/json" -d '{"days": 32}'

See Testing for more on test clocks.

A payment link with mode: "subscription" starts a subscription when paid: the payer enters an email, pays the first period, and their card is saved. No account or password. The plan sets the price, and we keep the recurring terms on the page as their mandate.

Terminal window
curl -sS https://api.tuppence.ai/v1/payment_links \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "Join the coffee club", "mode": "subscription", "plan": "plan_…"}'

A plan with a free trial can’t be sold from a link yet, because the card is saved by paying the first period. Start those with subscriptions.create.

  • No usage-based billing.
  • No invoices on their own: every invoice belongs to a subscription’s charge.
  • The portal changes the way to pay only to a card (a Direct Debit is set up by paying).
  • No tax calculation anywhere: see Not supported.

Docs assistant

Open as a page
Loading the assistant…