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.
Paystack recurring billing works in three steps:
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 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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// 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.
// 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);
}
}
// 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.
This guide is part of the Paystack integration guides by language and framework series.
Sign up for the McTaba newsletter for more Paystack guides.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course