Verify Paystack Payments in Firebase Cloud Functions
Create a Firebase Cloud Function using onRequest (v2). Read the reference from the query string or request body, call GET https://api.paystack.co/transaction/verify/:reference with your PAYSTACK_SECRET_KEY, check data.status === "success", update Firestore with the payment result, and return verified status to your frontend.
Verify 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 verifyPayment = onRequest(
{ secrets: [paystackSecretKey] },
async (req, res) => {
corsHandler(req, res, async () => {
const reference = req.query.reference as string || req.body?.reference;
if (!reference) {
res.status(400).json({ error: "Reference is required" });
return;
}
const secretKey = paystackSecretKey.value();
try {
const paystackRes = await fetch(
"https://api.paystack.co/transaction/verify/" + encodeURIComponent(reference),
{
headers: { Authorization: "Bearer " + secretKey },
}
);
const paystackData = await paystackRes.json();
if (!paystackData.status || paystackData.data.status !== "success") {
res.status(400).json({
verified: false,
status: paystackData.data?.status,
message: paystackData.data?.gateway_response || "Payment not verified",
});
return;
}
const txData = paystackData.data;
const db = getFirestore();
// Check idempotency
const orderRef = db.collection("orders").doc(reference);
const order = await orderRef.get();
if (order.exists && order.data()?.status === "paid") {
res.json({
verified: true,
amount: txData.amount / 100,
currency: txData.currency,
reference: txData.reference,
already_processed: true,
});
return;
}
// Update Firestore
await orderRef.set({
status: "paid",
paid_at: new Date().toISOString(),
amount_paid: txData.amount / 100,
currency: txData.currency,
customer_email: txData.customer.email,
}, { merge: true });
res.json({
verified: true,
amount: txData.amount / 100,
currency: txData.currency,
reference: txData.reference,
customer_email: txData.customer.email,
});
} catch (error) {
res.status(500).json({ error: "Verification failed" });
}
});
}
);
Call from Frontend
// After Paystack checkout redirect
async function verifyPayment(reference: string) {
var response = await fetch(
"https://<region>-<project-id>.cloudfunctions.net/verifyPayment?reference=" + reference
)
var result = await response.json()
if (result.verified) {
// Grant access, show success screen
console.log("Payment confirmed:", result.amount, result.currency)
} else {
// Show failure or retry
console.error("Verification failed:", result.message)
}
}
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Verification runs in the Cloud Function — the secret key never leaves the server.
- ✓Call GET https://api.paystack.co/transaction/verify/:reference with the Bearer token.
- ✓Validate data.status === "success" AND data.amount matches your expected amount before granting access.
- ✓Update Firestore after verification using the Firebase Admin SDK.
- ✓Use idempotency: check if the order is already marked paid before processing again.
- ✓Return a clean response to the frontend: verified, amount, currency, reference.
Frequently Asked Questions
- Do I need Firebase Admin SDK to update Firestore from a Cloud Function?
- Yes. Cloud Functions run in a trusted server environment, and you use the Firebase Admin SDK to access Firestore. Initialize with initializeApp() once at the top of your functions file. Then use getFirestore() to get the database. The Admin SDK bypasses Firestore security rules by default.
- What is the difference between verifying via webhook and verifying via a Cloud Function?
- Webhook verification is event-driven — Paystack calls your backend when a payment succeeds. Cloud Function verification is request-driven — your frontend calls the function after checkout redirect. Both should be implemented: webhooks for reliability, frontend verification for immediate UX feedback.
- Can I pass the reference in the request body instead of the query string?
- Yes. The example reads reference from both req.query.reference (GET) and req.body.reference (POST). For a GET request, use the query string. For a POST request from a trusted frontend, use the body. Either approach works.
- How do I prevent a Paystack transaction from being verified twice?
- Check Firestore before updating: if the order document already has status: "paid", return the result without re-processing. This is the idempotency pattern shown in the example. The already_processed flag in the response lets your frontend know the order was already confirmed.
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