Initialize a Paystack subscription by passing a plan code when calling /transaction/initialize in your Edge Function. After checkout, Paystack fires subscription.create and charge.success webhooks — handle them in a webhook Edge Function to update your Supabase users table with the subscription code, email token, and status.
// supabase/functions/subscribe/index.ts
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const secretKey = Deno.env.get("PAYSTACK_SECRET_KEY")!;
const { email, planCode } = await req.json();
if (!email || !planCode) {
return new Response(
JSON.stringify({ error: "email and planCode are required" }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const reference = "sub_" + Date.now() + "_" + crypto.randomUUID().split("-")[0];
const paystackRes = await fetch(
"https://api.paystack.co/transaction/initialize",
{
method: "POST",
headers: {
Authorization: "Bearer " + secretKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, plan: planCode, reference }),
}
);
const data = await paystackRes.json();
if (!data.status) {
return new Response(
JSON.stringify({ error: data.message }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
// Store pending subscription reference
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
await supabase.from("subscriptions").upsert({
email,
plan_code: planCode,
reference,
status: "pending",
created_at: new Date().toISOString(),
}, { onConflict: "email" });
return new Response(
JSON.stringify({
authorization_url: data.data.authorization_url,
reference: data.data.reference,
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
});
// In your webhook Edge Function, handle subscription events:
if (event.event === "subscription.create") {
const sub = event.data;
await supabase
.from("subscriptions")
.update({
status: "active",
subscription_code: sub.subscription_code,
email_token: sub.email_token,
next_billing_date: sub.next_payment_date,
activated_at: new Date().toISOString(),
})
.eq("email", sub.customer.email);
}
if (event.event === "charge.success" && event.data.plan) {
// Recurring charge on an existing subscription
await supabase
.from("subscriptions")
.update({
status: "active",
last_paid_at: new Date().toISOString(),
next_billing_date: event.data.plan.next_payment_date,
})
.eq("email", event.data.customer.email);
}
if (event.event === "invoice.payment_failed") {
await supabase
.from("subscriptions")
.update({ status: "past_due" })
.eq("email", event.data.customer.email);
}
if (event.event === "subscription.disable") {
await supabase
.from("subscriptions")
.update({ status: "cancelled" })
.eq("subscription_code", event.data.subscription_code);
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// supabase/functions/cancel-subscription/index.ts
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const { email } = await req.json();
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const { data: sub } = await supabase
.from("subscriptions")
.select("subscription_code, email_token")
.eq("email", email)
.eq("status", "active")
.single();
if (!sub) {
return new Response(
JSON.stringify({ error: "No active subscription found" }),
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const paystackRes = await fetch(
"https://api.paystack.co/subscription/disable",
{
method: "POST",
headers: {
Authorization: "Bearer " + Deno.env.get("PAYSTACK_SECRET_KEY"),
"Content-Type": "application/json",
},
body: JSON.stringify({
code: sub.subscription_code,
token: sub.email_token,
}),
}
);
const data = await paystackRes.json();
if (!data.status) {
return new Response(
JSON.stringify({ error: data.message }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
await supabase
.from("subscriptions")
.update({ status: "cancelled" })
.eq("email", email);
return new Response(
JSON.stringify({ cancelled: true }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
});
This guide is part of the Paystack integration guides by language and framework series.
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