Create a Firebase Cloud Function using onRequest (v2). Access req.rawBody for HMAC SHA512 verification with crypto.createHmac. Compare the hash against the x-paystack-signature header, send 200 immediately, then update Firestore using the Firebase Admin SDK.
// 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 crypto from "crypto";
initializeApp();
const paystackSecretKey = defineSecret("PAYSTACK_SECRET_KEY");
export const paystackWebhook = onRequest(
{ secrets: [paystackSecretKey] },
async (req, res) => {
if (req.method !== "POST") {
res.status(405).send("Method not allowed");
return;
}
const signature = req.headers["x-paystack-signature"] as string;
const secretKey = paystackSecretKey.value();
// req.rawBody is a Buffer available in Firebase Cloud Functions
const hash = crypto
.createHmac("sha512", secretKey)
.update(req.rawBody)
.digest("hex");
if (hash !== signature) {
res.status(400).send("Invalid signature");
return;
}
// Return 200 before processing
res.sendStatus(200);
const event = req.body;
const db = getFirestore();
// Idempotency check
const eventId = event.data?.id?.toString() ?? event.data?.reference;
if (eventId) {
const processedRef = db.collection("webhook_events").doc(eventId);
const existing = await processedRef.get();
if (existing.exists) return;
await processedRef.set({
event_type: event.event,
processed_at: new Date().toISOString(),
});
}
switch (event.event) {
case "charge.success": {
const reference = event.data.reference;
const amount = event.data.amount / 100;
await db.collection("orders").doc(reference).set({
status: "paid",
paid_at: new Date().toISOString(),
amount_paid: amount,
currency: event.data.currency,
customer_email: event.data.customer.email,
}, { merge: true });
break;
}
case "transfer.success": {
const transferCode = event.data.transfer_code;
await db.collection("transfers")
.where("transfer_code", "==", transferCode)
.get()
.then(snapshot => {
snapshot.forEach(doc => doc.ref.update({ status: "success" }));
});
break;
}
case "invoice.payment_failed": {
const email = event.data.customer.email;
await db.collection("subscriptions")
.where("email", "==", email)
.get()
.then(snapshot => {
snapshot.forEach(doc => doc.ref.update({ status: "past_due" }));
});
break;
}
}
}
);
# Set Paystack secret key as a Firebase secret
firebase functions:secrets:set PAYSTACK_SECRET_KEY
# Deploy only the webhook function
firebase deploy --only functions:paystackWebhook
After deploying, add the function URL to your Paystack dashboard webhook settings:
https://<region>-<project-id>.cloudfunctions.net/paystackWebhook
Create the Firestore security rules for webhook_events (server-only access):
match /webhook_events/{eventId} {
allow read, write: if false; // Only Admin SDK can access
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
This guide is part of the Paystack integration guides by language and framework series.
Next: Build Paystack Subscriptions in Firebase Cloud Functions
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