Bonaventure OgetoBy Bonaventure Ogeto|

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

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