Build Paystack Subscriptions in Firebase Cloud Functions
Pass a plan code in your Cloud Function when calling /transaction/initialize to start a subscription. After checkout, Paystack fires subscription.create — handle it in your webhook Cloud Function to store the subscription_code and email_token in Firestore. Read subscription status from Firestore in your app rather than calling Paystack directly.
Subscribe Cloud Function
// functions/src/index.ts
import { onRequest } from "firebase-functions/v2/https";
import { defineSecret } from "firebase-functions/params";
import { getFirestore } from "firebase-admin/firestore";
import { initializeApp } from "firebase-admin/app";
import * as cors from "cors";
initializeApp();
const paystackSecretKey = defineSecret("PAYSTACK_SECRET_KEY");
const corsHandler = cors({ origin: true });
export const subscribe = onRequest(
{ secrets: [paystackSecretKey] },
async (req, res) => {
corsHandler(req, res, async () => {
if (req.method !== "POST") {
res.status(405).send("Method not allowed");
return;
}
const { email, planCode } = req.body;
if (!email || !planCode) {
res.status(400).json({ error: "email and planCode are required" });
return;
}
const reference = "sub_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
const secretKey = paystackSecretKey.value();
try {
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) {
res.status(400).json({ error: data.message });
return;
}
// Store pending subscription in Firestore
const db = getFirestore();
await db.collection("subscriptions").doc(email).set({
email,
plan_code: planCode,
reference,
status: "pending",
created_at: new Date().toISOString(),
}, { merge: true });
res.json({
authorization_url: data.data.authorization_url,
reference: data.data.reference,
});
} catch {
res.status(500).json({ error: "Failed to initialize subscription" });
}
});
}
);
Subscription Webhook Events
// In your paystackWebhook Cloud Function, add subscription event handling:
case "subscription.create": {
const sub = event.data;
const db = getFirestore();
await db.collection("subscriptions").doc(sub.customer.email).set({
status: "active",
subscription_code: sub.subscription_code,
email_token: sub.email_token,
plan_name: sub.plan?.name,
next_billing_date: sub.next_payment_date,
activated_at: new Date().toISOString(),
}, { merge: true });
break;
}
case "charge.success": {
// Recurring billing charge on an active subscription
if (event.data.plan) {
const db = getFirestore();
await db.collection("subscriptions").doc(event.data.customer.email).update({
status: "active",
last_paid_at: new Date().toISOString(),
});
}
break;
}
case "invoice.payment_failed": {
const db = getFirestore();
await db.collection("subscriptions").doc(event.data.customer.email).update({
status: "past_due",
last_failed_at: new Date().toISOString(),
});
break;
}
case "subscription.disable": {
const db = getFirestore();
const snapshot = await db.collection("subscriptions")
.where("subscription_code", "==", event.data.subscription_code)
.get();
snapshot.forEach(doc => doc.ref.update({ status: "cancelled" }));
break;
}
Cancel Subscription
export const cancelSubscription = onRequest(
{ secrets: [paystackSecretKey] },
async (req, res) => {
corsHandler(req, res, async () => {
const { email } = req.body;
const db = getFirestore();
const subDoc = await db.collection("subscriptions").doc(email).get();
const sub = subDoc.data();
if (!sub || sub.status !== "active") {
res.status(404).json({ error: "No active subscription found" });
return;
}
const secretKey = paystackSecretKey.value();
const paystackRes = await fetch(
"https://api.paystack.co/subscription/disable",
{
method: "POST",
headers: {
Authorization: "Bearer " + secretKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
code: sub.subscription_code,
token: sub.email_token,
}),
}
);
const data = await paystackRes.json();
if (!data.status) {
res.status(400).json({ error: data.message });
return;
}
await db.collection("subscriptions").doc(email).update({
status: "cancelled",
cancelled_at: new Date().toISOString(),
});
res.json({ cancelled: true });
});
}
);
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Start a subscription by passing a plan code to /transaction/initialize in your Cloud Function.
- ✓After the first successful payment, Paystack fires subscription.create with subscription_code and email_token.
- ✓Store subscription_code and email_token in Firestore — you need both to cancel via /subscription/disable.
- ✓Handle invoice.payment_failed to mark subscriptions as past_due and notify users.
- ✓Read subscription status from Firestore in your app. Avoid calling the Paystack API on every page load.
- ✓To cancel, call your Cloud Function which calls POST /subscription/disable with code and token.
Frequently Asked Questions
- How do I create a Paystack plan for use with Firebase Cloud Functions?
- Create the plan once in the Paystack dashboard or via POST /plan with your secret key. You get a plan code (PLN_xxxx). Pass this as the plan parameter in your /transaction/initialize call. Paystack handles all recurring billing automatically after the first payment.
- Can I store subscription data in Firestore instead of a SQL database?
- Yes. Firestore works well for subscription state. Use the customer email as the document ID for easy lookups. Store subscription_code, email_token, status, and next_billing_date. Update these fields from your webhook Cloud Function on each subscription event.
- What happens if the webhook fires but the Firestore write fails?
- Paystack retries webhooks for up to 72 hours if your endpoint returns a non-200 response. Since you return 200 before processing, Paystack will not retry. Protect against missed writes by also verifying subscription status through the Paystack API endpoint if discrepancies are found.
- How do I check subscription status in my Flutter or React Native app?
- Create a getSubscriptionStatus Cloud Function that reads from Firestore by email. Call this on app launch and after checkout. Do not call the Paystack API directly from mobile — it requires the secret key. Your Cloud Function is the right place to fetch and relay subscription state.
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