Subscriptions
Set up recurring billing with plan management and lifecycle handling
This recipe walks through implementing recurring billing with Hanzo Commerce -- creating plans, managing billing cycles, handling upgrades and downgrades, cancellation flows, and reacting to payment events via webhooks.
What You Will Build
- Subscription plans with monthly and annual billing
- Customer subscription management (create, upgrade, downgrade, cancel)
- Webhook handlers for payment events
- A self-service portal for customers
Create Subscription Plans
Define your pricing tiers. Each plan is a product with type: 'subscription' and a billingInterval.
import { Commerce } from '@hanzo/commerce'
const commerce = new Commerce({
apiKey: process.env.HANZO_API_KEY!,
environment: 'production',
})
// Starter plan
const starter = await commerce.products.create({
name: 'Starter',
slug: 'starter',
type: 'subscription',
price: 1900, // $19/month
currency: 'USD',
billingInterval: 'month',
metadata: { tier: 'starter', features: 'basic' },
})
// Pro plan -- monthly
const proMonthly = await commerce.products.create({
name: 'Pro Monthly',
slug: 'pro-monthly',
type: 'subscription',
price: 4900, // $49/month
currency: 'USD',
billingInterval: 'month',
metadata: { tier: 'pro', features: 'advanced' },
})
// Pro plan -- annual (save ~17%)
const proAnnual = await commerce.products.create({
name: 'Pro Annual',
slug: 'pro-annual',
type: 'subscription',
price: 49000, // $490/year
currency: 'USD',
billingInterval: 'year',
metadata: { tier: 'pro', features: 'advanced' },
})Create a Subscription
When a customer selects a plan, create a subscription through checkout:
const session = await commerce.checkout.create({
mode: 'subscription',
items: [{ productId: proMonthly.id, quantity: 1 }],
customerId: customer.id,
successUrl: 'https://app.example.com/dashboard?subscribed=true',
cancelUrl: 'https://app.example.com/pricing',
})
// Redirect customer to session.urlAfter successful payment, Commerce creates a Subscription object:
{
"id": "sub_abc123",
"customerId": "cust_def456",
"productId": "prod_pro_monthly",
"status": "active",
"currentPeriodStart": "2026-02-16T00:00:00Z",
"currentPeriodEnd": "2026-03-16T00:00:00Z",
"cancelAtPeriodEnd": false
}Subscription Lifecycle
Status Flow
created --> active --> past_due --> canceled
\-> paused
\-> canceled (immediate)| Status | Description |
|---|---|
created | Subscription created, awaiting first payment |
active | Payments current, access granted |
past_due | Payment failed, retry in progress |
paused | Manually paused by customer or admin |
canceled | Subscription ended |
Billing Cycle
Commerce handles billing automatically:
- 3 days before renewal:
subscription.upcomingevent fires - On renewal date: Payment is attempted
- If payment fails: Status moves to
past_due, retries at 1, 3, and 7 days - After all retries fail: Status moves to
canceled,subscription.canceledevent fires
Upgrades and Downgrades
Immediate Upgrade
Prorate the current period and charge the difference immediately:
await commerce.subscriptions.update(subscriptionId, {
productId: proAnnual.id,
proration: 'immediate',
})Downgrade at Period End
Apply the change when the current billing period ends:
await commerce.subscriptions.update(subscriptionId, {
productId: starter.id,
proration: 'next_period',
})Proration Modes
| Mode | Behavior |
|---|---|
immediate | Charge/credit difference now, switch plan now |
next_period | Switch plan at next billing date, no proration |
always_invoice | Generate an invoice for the proration amount |
Cancellation Flows
Cancel at Period End (Recommended)
The customer retains access until the current period expires:
await commerce.subscriptions.cancel(subscriptionId, {
mode: 'at_period_end',
})Cancel Immediately
End the subscription now and optionally issue a prorated refund:
await commerce.subscriptions.cancel(subscriptionId, {
mode: 'immediate',
refund: 'prorated', // or 'none' or 'full'
})Reactivate a Canceled Subscription
If the customer cancels but changes their mind before the period ends:
await commerce.subscriptions.reactivate(subscriptionId)Webhook Integration
Register webhooks to react to subscription events in your application.
await commerce.webhooks.create({
url: 'https://app.example.com/api/webhooks/commerce',
events: [
'subscription.created',
'subscription.updated',
'subscription.canceled',
'subscription.past_due',
'invoice.paid',
'invoice.payment_failed',
],
})Webhook Handler
import { commerce } from '@/lib/commerce'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const signature = req.headers.get('x-commerce-signature')!
const body = await req.text()
const event = commerce.webhooks.verify(body, signature)
switch (event.type) {
case 'subscription.created':
// Grant access to the customer
await grantAccess(event.data.customerId, event.data.productId)
break
case 'subscription.canceled':
// Revoke access (respecting cancelAtPeriodEnd)
if (!event.data.cancelAtPeriodEnd) {
await revokeAccess(event.data.customerId, event.data.productId)
}
break
case 'subscription.past_due':
// Send a warning email, optionally restrict features
await notifyPastDue(event.data.customerId)
break
case 'invoice.payment_failed':
// Log the failure, notify the customer
await notifyPaymentFailed(event.data.customerId, event.data.invoiceId)
break
}
return NextResponse.json({ received: true })
}Self-Service Portal
Let customers manage their own subscriptions:
// Generate a portal session URL
const portal = await commerce.billing.createPortalSession({
customerId: customer.id,
returnUrl: 'https://app.example.com/settings',
})
// Redirect customer to portal.urlThe portal allows customers to:
- View current plan and billing history
- Upgrade or downgrade their plan
- Update payment method
- Cancel their subscription
- Download invoices
Checking Subscription Status
Gate features in your application by checking subscription status:
const subscription = await commerce.subscriptions.getByCustomer(customerId)
if (subscription?.status === 'active') {
const tier = subscription.product.metadata.tier
// Grant tier-appropriate features
}Testing
- Create subscription plans via the SDK
- Complete a subscription checkout with Stripe test card
4242 4242 4242 4242 - Verify the subscription status is
active - Test upgrade: switch from Starter to Pro, confirm proration
- Test cancellation: cancel at period end, verify
cancelAtPeriodEndistrue - Use Stripe test card
4000 0000 0000 0341to simulate payment failure and verifypast_duehandling
Next Steps
- Combine with digital products for SaaS licensing
- Add multi-currency support for international subscribers
- Review the Subscription module for advanced configuration
How is this guide?
Last updated on