Build Paystack Subscriptions in Svelte and SvelteKit
To build Paystack subscriptions in SvelteKit, create a plan via the Paystack API, then initialize a transaction with plan code to subscribe a customer. Paystack handles recurring billing automatically. Listen for subscription.create, invoice.payment_failed, and charge.success webhooks in a +server.ts endpoint to manage subscription state in your database.
How Paystack Subscriptions Work
Paystack recurring billing works in three steps:
- Create a plan. A plan defines the amount, currency, and billing interval (daily, weekly, monthly, annually).
- Initialize a transaction with a plan code. The customer pays the first invoice. Paystack tokenizes their card and creates a subscription.
- Automatic renewals. Paystack charges the card on each billing date. You receive webhooks for successful and failed charges.
You do not trigger renewals. Paystack handles the billing cycle. Your job is to respond to webhooks and update your database accordingly.
Create a Plan
Create a plan in the Paystack dashboard or via API. Once created, the plan code is stable. You only need to create it once:
// src/routes/api/admin/create-plan/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json } from '@sveltejs/kit';
export async function POST({ request }) {
var body = await request.json();
var response = await fetch('https://api.paystack.co/plan', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: body.name, // e.g. "Pro Monthly"
amount: body.amount, // in kobo, e.g. 500000 = NGN 5,000
interval: body.interval, // monthly, weekly, annually, daily
currency: body.currency || 'NGN',
}),
});
var data = await response.json();
return json(data);
}
Store the returned plan_code (e.g., PLN_xxxxxxxxxxxx) in your app config or database. You will use it when subscribing customers.
Subscribe a Customer
// src/routes/api/subscribe/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json, error } from '@sveltejs/kit';
var PLAN_CODE = 'PLN_xxxxxxxxxxxx'; // Your plan code from above
export async function POST({ request }) {
var body = await request.json();
var email = body.email;
if (!email) {
throw error(400, 'Email required');
}
var reference = 'sub_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
plan: PLAN_CODE, // Linking to a plan creates a subscription
reference: reference,
callback_url: 'https://yourapp.com/subscription/callback',
}),
});
var data = await response.json();
if (!data.status) {
throw error(400, data.message);
}
return json({
authorization_url: data.data.authorization_url,
access_code: data.data.access_code,
reference: data.data.reference,
});
}
When you include a plan in transaction initialize, Paystack ignores any amount you passed (plan amount is used), tokenizes the card after successful payment, and automatically charges it on each renewal date.
Subscription Webhook Handler
// src/routes/api/webhooks/paystack/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json, error } from '@sveltejs/kit';
import { createHmac } from 'crypto';
export async function POST({ request }) {
var rawBody = await request.text();
var signature = request.headers.get('x-paystack-signature');
if (!signature) throw error(400, 'No signature');
var hash = createHmac('sha512', PAYSTACK_SECRET_KEY)
.update(rawBody)
.digest('hex');
if (hash !== signature) throw error(400, 'Invalid signature');
var event = JSON.parse(rawBody);
handleSubscriptionEvent(event).catch(console.error);
return json({ received: true });
}
async function handleSubscriptionEvent(event: { event: string; data: Record }) {
switch (event.event) {
case 'subscription.create': {
var sub = event.data;
var email = (sub.customer as { email: string }).email;
var subCode = sub.subscription_code as string;
console.log('New subscription:', subCode, email);
// Activate user's plan in database
break;
}
case 'charge.success': {
var charge = event.data;
var plan = charge.plan as { plan_code: string } | null;
if (plan) {
// Subscription renewal
console.log('Renewal paid:', charge.reference, charge.amount);
// Extend subscription period in database
}
break;
}
case 'invoice.payment_failed': {
var invoice = event.data;
var subscription = invoice.subscription as { subscription_code: string };
console.log('Renewal failed:', subscription.subscription_code);
// Pause access, send notification email
break;
}
case 'subscription.disable': {
console.log('Subscription cancelled:', event.data);
break;
}
default:
console.log('Unhandled event:', event.event);
}
}
Cancel a Subscription
// src/routes/api/subscription/cancel/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json, error } from '@sveltejs/kit';
export async function POST({ request }) {
var body = await request.json();
var subscriptionCode = body.subscription_code;
var emailToken = body.email_token;
if (!subscriptionCode || !emailToken) {
throw error(400, 'subscription_code and email_token required');
}
var response = await fetch('https://api.paystack.co/subscription/disable', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: subscriptionCode,
token: emailToken,
}),
});
var data = await response.json();
if (!data.status) {
throw error(400, data.message);
}
return json({ cancelled: true });
}
The email_token comes from the subscription.create webhook payload. Store it alongside the subscription code in your database when the subscription is created.
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Sign up for the McTaba newsletter for more Paystack guides.
Key Takeaways
- ✓Create a Paystack plan once (via API or dashboard) and store the plan code in your app config.
- ✓Subscribe a customer by passing the plan code to transaction initialize. Paystack charges the card and creates the subscription automatically.
- ✓After the first payment, Paystack handles all renewals. You receive charge.success and invoice webhooks.
- ✓Handle invoice.payment_failed to pause access when a renewal fails and notify the customer.
- ✓The subscription_code in the charge.success webhook lets you link payments to subscriptions in your database.
- ✓To cancel, call POST /subscription/disable with the subscription code and email token from a server endpoint.
Frequently Asked Questions
- How does Paystack know which card to charge on renewal?
- When a customer completes the first transaction on a plan, Paystack tokenizes their card and stores the authorization. Renewals use this stored authorization. You do not need to handle card storage yourself.
- What happens if a renewal payment fails?
- Paystack fires an invoice.payment_failed webhook. Listen for this and pause the customer access while notifying them to update their payment method.
- Can I change a plan amount for existing subscribers?
- No. Existing subscribers stay on the plan amount they signed up for. To change amounts for existing customers, cancel their subscription and create a new one on the updated plan.
- How do I let customers manage their subscription?
- Store the subscription_code from the subscription.create webhook. Use it to call the Paystack subscription API from server endpoints. Paystack also sends management emails with links customers can use directly.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon