McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Build Paystack Subscriptions in Supabase Edge Functions
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build Paystack Subscriptions in Supabase Edge Functions

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.

In this article

  1. 1. Subscribe Edge Function
  2. 2. Subscription Webhook Handler
  3. 3. Cancel Subscription
  4. 4. Learn More

Subscribe Edge Function

// 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" } }
  );
});

Subscription Webhook Handler

// 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.

Cancel Subscription

// 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" } }
  );
});

Learn More

This guide is part of the Paystack integration guides by language and framework series.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Subscribe a user by passing a plan code in the transaction/initialize call from your Edge Function.
  • ✓After checkout, Paystack fires subscription.create with the subscription_code and email_token — store both in Supabase.
  • ✓Handle invoice.payment_failed to mark subscriptions as past_due and send a payment recovery email.
  • ✓Store subscription_code and email_token in your database — you need them to cancel via /subscription/disable.
  • ✓Expose a subscription status endpoint that reads from Supabase, not Paystack, to avoid extra API calls.
  • ✓Use the service role key in Edge Functions to bypass RLS when writing subscription data server-side.

Frequently Asked Questions

How do I create a plan in Paystack for Edge Function subscriptions?
Create the plan once in the Paystack dashboard or via the API (POST /plan). Note the plan code (PLN_xxxx). Pass this as the plan parameter in your /transaction/initialize call. Paystack automatically sets up the subscription after a successful payment.
Where do I get the email_token for cancelling a subscription?
Paystack includes email_token in the subscription.create webhook payload. Store it in your database alongside the subscription_code. You need both to call /subscription/disable. Without the email_token, you cannot cancel via the API.
What is the difference between subscription.create and charge.success for recurring billing?
subscription.create fires once when the subscription is first created (after the initial payment). charge.success fires for every subsequent recurring charge. Handle both to keep your subscription status accurate.
Can I access subscription plans list from a Supabase Edge Function?
Yes. Call GET https://api.paystack.co/plan with your Paystack secret key from Deno.env. This returns all your plans. Cache the response or store plan details in Supabase to avoid repeated API calls from your frontend.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Accept Payments with Paystack in Supabase Edge Functions

Supabase Edge Functions run Deno TypeScript at the edge. Use them as your Paystack backend to initialize transactions, verify payments, and handle webhooks without deploying a separate server.

Read more

Verify Paystack Payments in Supabase Edge Functions

Verify Paystack payments using Supabase Edge Functions written in Deno TypeScript. This guide covers calling the verify endpoint, validating amounts, updating your Supabase database after confirmation, and calling the function from your frontend.

Read more

Handle Paystack Webhooks in Supabase Edge Functions

Receive Paystack webhook events in a Supabase Edge Function written in Deno TypeScript. This guide covers HMAC SHA512 signature verification using the Web Crypto API, returning 200 before processing, and updating your Supabase database after confirming the event.

Read more

Handling subscription.create, subscription.disable and invoice Events

A complete walkthrough of every Paystack subscription and invoice webhook event. Covers subscription.create, subscription.not_renew, subscription.disable, invoice.create, invoice.payment_failed, and invoice.update with production-ready Node.js handlers.

Read more

Paystack Card Not Supported for Recurring

When Paystack says a card is not supported for recurring charges, the card type or issuing bank blocks tokenization. This guide covers why it happens, which card types are affected, how to handle it gracefully, and alternative billing strategies.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.