Subscriptions
Create and manage recurring billing with subscriptions.
Subscriptions
Subscriptions allow you to charge customers on a recurring basis. Perfect for SaaS, memberships, and any recurring billing.
Overview
PayGate Subscriptions include:
- Flexible billing intervals - Daily, weekly, monthly, or yearly
- Trial periods - Give customers time to try before they buy
- Prorations - Handle upgrades and downgrades fairly
- Automatic retries - Smart retry logic for failed payments
- Lifecycle webhooks - Stay informed of subscription changes
How It Works
Create a Plan
Define a pricing plan with amount and billing interval.
Subscribe a Customer
Create a subscription linking a customer to a plan.
Automatic Billing
PayGate automatically charges the customer each billing cycle.
Handle Events
Receive webhooks for successful payments, failures, and cancellations.
Create a Plan
First, create a plan that defines your pricing:
const plan = await paygate.plans.create({
name: 'Pro Monthly',
amount: 5000, // GHS 50.00
currency: 'GHS',
interval: 'month',
interval_count: 1, // Every 1 month
trial_period_days: 14, // 14-day free trial
metadata: {
features: 'unlimited_access,priority_support'
}
})
console.log(plan.id) // plan_abc123plan = client.plans.create(
name='Pro Monthly',
amount=5000,
currency='GHS',
interval='month',
interval_count=1,
trial_period_days=14,
metadata={'features': 'unlimited_access,priority_support'}
)
print(plan.id) # plan_abc123$plan = $paygate->plans->create([
'name' => 'Pro Monthly',
'amount' => 5000,
'currency' => 'GHS',
'interval' => 'month',
'interval_count' => 1,
'trial_period_days' => 14,
'metadata' => ['features' => 'unlimited_access,priority_support']
]);
echo $plan->id; // plan_abc123curl -X POST https://api.44.200.142.19.nip.io/v1/plans \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Pro Monthly",
"amount": 5000,
"currency": "GHS",
"interval": "month",
"interval_count": 1,
"trial_period_days": 14
}'Plan Intervals
| Interval | Example |
|---|---|
day | Charge every day |
week | Charge every week |
month | Charge every month |
year | Charge every year |
Use interval_count for custom intervals:
// Charge every 3 months (quarterly)
const quarterlyPlan = await paygate.plans.create({
name: 'Quarterly Plan',
amount: 12000,
currency: 'GHS',
interval: 'month',
interval_count: 3
})
// Charge every 2 weeks (bi-weekly)
const biweeklyPlan = await paygate.plans.create({
name: 'Bi-weekly Plan',
amount: 2000,
currency: 'GHS',
interval: 'week',
interval_count: 2
})Create a Subscription
Subscribe a customer to a plan:
// First, create or retrieve a customer
const customer = await paygate.customers.create({
email: 'customer@example.com',
name: 'John Doe',
phone: '0241234567'
})
// Create the subscription
const subscription = await paygate.subscriptions.create({
customer: customer.id,
plan: 'plan_abc123',
payment_method: {
type: 'mobile_money',
phone: '0241234567',
provider: 'mtn'
}
})
console.log(subscription.status) // 'trialing' or 'active'customer = client.customers.create(
email='customer@example.com',
name='John Doe',
phone='0241234567'
)
subscription = client.subscriptions.create(
customer=customer.id,
plan='plan_abc123',
payment_method={
'type': 'mobile_money',
'phone': '0241234567',
'provider': 'mtn'
}
)
print(subscription.status) # 'trialing' or 'active'$customer = $paygate->customers->create([
'email' => 'customer@example.com',
'name' => 'John Doe',
'phone' => '0241234567'
]);
$subscription = $paygate->subscriptions->create([
'customer' => $customer->id,
'plan' => 'plan_abc123',
'payment_method' => [
'type' => 'mobile_money',
'phone' => '0241234567',
'provider' => 'mtn'
]
]);
echo $subscription->status; // 'trialing' or 'active'Subscription Object
{
"id": "sub_abc123def456",
"object": "subscription",
"status": "active",
"customer": "cus_xyz789",
"plan": "plan_abc123",
"current_period_start": "2024-01-15T00:00:00Z",
"current_period_end": "2024-02-15T00:00:00Z",
"trial_start": null,
"trial_end": null,
"cancel_at_period_end": false,
"canceled_at": null,
"metadata": {},
"created_at": "2024-01-15T10:00:00Z"
}Subscription Statuses
| Status | Description |
|---|---|
trialing | In free trial period |
active | Subscription is active and billing |
past_due | Payment failed, retrying |
canceled | Subscription was canceled |
unpaid | All retry attempts failed |
paused | Subscription is paused |
Manage Subscriptions
Retrieve a Subscription
const subscription = await paygate.subscriptions.retrieve('sub_abc123')List Customer Subscriptions
const subscriptions = await paygate.subscriptions.list({
customer: 'cus_xyz789',
status: 'active'
})Cancel a Subscription
// Cancel at period end (recommended)
const subscription = await paygate.subscriptions.update('sub_abc123', {
cancel_at_period_end: true
})
// Cancel immediately
const subscription = await paygate.subscriptions.cancel('sub_abc123')Pause a Subscription
const subscription = await paygate.subscriptions.pause('sub_abc123')
// Resume later
const subscription = await paygate.subscriptions.resume('sub_abc123')Change Plans
// Upgrade or downgrade
const subscription = await paygate.subscriptions.update('sub_abc123', {
plan: 'plan_premium_monthly',
proration_behavior: 'create_prorations' // Charge/credit the difference
})Trial Periods
Plan-Level Trials
Set a default trial when creating a plan:
const plan = await paygate.plans.create({
name: 'Pro Monthly',
amount: 5000,
currency: 'GHS',
interval: 'month',
trial_period_days: 14 // All subscriptions get 14-day trial
})Subscription-Level Trials
Override the trial period for specific subscriptions:
// Give this customer a longer trial
const subscription = await paygate.subscriptions.create({
customer: 'cus_xyz789',
plan: 'plan_abc123',
trial_period_days: 30 // 30-day trial instead of plan default
})
// No trial for this customer
const subscription = await paygate.subscriptions.create({
customer: 'cus_xyz789',
plan: 'plan_abc123',
trial_period_days: 0 // Skip trial
})Handle Failed Payments
When a subscription payment fails, PayGate:
- Sets subscription status to
past_due - Sends
subscription.payment_failedwebhook - Retries according to your retry schedule
Configure Retry Schedule
Set your retry schedule in the Dashboard or via API:
// Configure retry settings for your account
await paygate.billing.settings.update({
subscription_retry_schedule: [1, 3, 5, 7], // Retry on days 1, 3, 5, 7
subscription_cancel_after_retries: true // Cancel after all retries fail
})Handle Past Due Subscriptions
app.post('/webhooks', (req, res) => {
const event = Webhook.constructEvent(req.body, signature, secret)
if (event.type === 'subscription.payment_failed') {
const subscription = event.data.object
const customer = subscription.customer
// Notify customer
await sendPaymentFailedEmail(customer)
// Maybe restrict access
await restrictCustomerAccess(customer)
}
if (event.type === 'subscription.updated') {
const subscription = event.data.object
if (subscription.status === 'active') {
// Payment succeeded, restore access
await restoreCustomerAccess(subscription.customer)
}
}
res.json({ received: true })
})Webhook Events
| Event | Description |
|---|---|
subscription.created | New subscription created |
subscription.updated | Subscription was updated |
subscription.canceled | Subscription was canceled |
subscription.paused | Subscription was paused |
subscription.resumed | Subscription was resumed |
subscription.trial_will_end | Trial ending in 3 days |
subscription.payment_succeeded | Billing succeeded |
subscription.payment_failed | Billing failed |
The subscription.trial_will_end event is sent 3 days before a trial ends, giving you time to remind customers.
Best Practices
1. Handle Trial Ending
Remind customers before their trial ends:
if (event.type === 'subscription.trial_will_end') {
const subscription = event.data.object
await sendTrialEndingEmail(subscription.customer, {
trial_end: subscription.trial_end,
amount: subscription.plan.amount
})
}2. Graceful Cancellation
Use cancel_at_period_end to let customers use their remaining time:
// Don't cancel immediately
const subscription = await paygate.subscriptions.update('sub_abc123', {
cancel_at_period_end: true
})
// Customer can still use service until period ends3. Store Subscription Status
Keep subscription status in your database:
// When subscription is created or updated
await db.users.update({
where: { id: customer.metadata.user_id },
data: {
subscription_status: subscription.status,
subscription_id: subscription.id,
current_period_end: subscription.current_period_end
}
})4. Check Access
Use subscription status to control access:
async function hasActiveSubscription(userId: string): Promise<boolean> {
const user = await db.users.findUnique({ where: { id: userId } })
if (!user.subscription_id) return false
const subscription = await paygate.subscriptions.retrieve(user.subscription_id)
return ['active', 'trialing'].includes(subscription.status)
}