Verify Paystack Payments in Supabase Edge Functions
Create a Supabase Edge Function at supabase/functions/verify/index.ts. Read the transaction reference from the URL query string, call GET https://api.paystack.co/transaction/verify/:reference with your PAYSTACK_SECRET_KEY from Deno.env, check data.status === "success", update your Supabase database with the Supabase client, and return the result.
Verify Edge Function
// supabase/functions/verify/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 url = new URL(req.url);
const reference = url.searchParams.get("reference");
if (!reference) {
return new Response(
JSON.stringify({ error: "Reference is required" }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const secretKey = Deno.env.get("PAYSTACK_SECRET_KEY");
// Call Paystack verify
const paystackResponse = await fetch(
"https://api.paystack.co/transaction/verify/" + encodeURIComponent(reference),
{ headers: { Authorization: "Bearer " + secretKey } }
);
const paystackData = await paystackResponse.json();
if (!paystackData.status || paystackData.data.status !== "success") {
return new Response(
JSON.stringify({
verified: false,
status: paystackData.data?.status,
message: paystackData.data?.gateway_response || "Payment not verified",
}),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const txData = paystackData.data;
// Update database using service role key (bypasses RLS)
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const { error: dbError } = await supabase
.from("orders")
.update({
status: "paid",
paid_at: new Date().toISOString(),
amount_paid: txData.amount / 100,
currency: txData.currency,
})
.eq("reference", reference);
if (dbError) {
console.error("DB update failed:", dbError.message);
}
return new Response(
JSON.stringify({
verified: true,
amount: txData.amount / 100,
currency: txData.currency,
reference: txData.reference,
customer_email: txData.customer.email,
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
});
Deploy and Call
supabase functions deploy verify
// Frontend call
const { data, error } = await supabase.functions.invoke('verify', {
method: 'GET',
// or pass as query param:
})
// Or direct fetch:
var response = await fetch(
process.env.SUPABASE_URL + '/functions/v1/verify?reference=' + reference,
{
headers: { 'Authorization': 'Bearer ' + process.env.SUPABASE_ANON_KEY }
}
)
var result = await response.json()
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Verification happens in the Edge Function, not on the frontend. The secret key stays in Deno.env.
- ✓Use native fetch in Deno to call the Paystack verify endpoint.
- ✓After verification, use the Supabase service role key to update the database (bypasses RLS).
- ✓Return a clean response to the frontend — verified, amount, currency, reference.
- ✓Always check both status ("success") and amount against your database record before granting access.
- ✓Log failed verifications for debugging. Paystack test mode returns real responses that test your logic.
Frequently Asked Questions
- Why use the service role key for database updates in Edge Functions?
- The service role key bypasses Row Level Security (RLS) policies. When a payment is verified server-side, your application logic has determined the action is legitimate. The anon key would be subject to RLS policies that might block updates from the Edge Function context.
- Can I verify and update the database in the same Edge Function?
- Yes, and you should. The verify Edge Function in this guide does both: calls Paystack to verify, then updates the Supabase orders table. This is the right approach — keep the entire verification flow in one serverless function.
- What is the difference between SUPABASE_URL and SUPABASE_ANON_KEY in Edge Functions?
- SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY are automatically available in Edge Functions via Deno.env. You do not need to set them manually. They are injected by the Supabase runtime. Only external secrets like PAYSTACK_SECRET_KEY need to be set with supabase secrets set.
- How do I test Edge Function verification locally?
- Run supabase functions serve verify. Test with curl: curl "http://localhost:54321/functions/v1/verify?reference=test_ref". Use test API keys and Paystack test mode for local development. The function runs identically locally and in production.
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