PayGate

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_abc123
plan = 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_abc123
curl -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

IntervalExample
dayCharge every day
weekCharge every week
monthCharge every month
yearCharge 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

StatusDescription
trialingIn free trial period
activeSubscription is active and billing
past_duePayment failed, retrying
canceledSubscription was canceled
unpaidAll retry attempts failed
pausedSubscription 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:

  1. Sets subscription status to past_due
  2. Sends subscription.payment_failed webhook
  3. 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

EventDescription
subscription.createdNew subscription created
subscription.updatedSubscription was updated
subscription.canceledSubscription was canceled
subscription.pausedSubscription was paused
subscription.resumedSubscription was resumed
subscription.trial_will_endTrial ending in 3 days
subscription.payment_succeededBilling succeeded
subscription.payment_failedBilling 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 ends

3. 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)
}