Create a Supabase Edge Function at supabase/functions/webhook/index.ts. Read the raw body with await req.text(), compute HMAC SHA512 using crypto.subtle with your PAYSTACK_SECRET_KEY from Deno.env, compare against the x-paystack-signature header, return 200 immediately, then update your Supabase database with the service role key.
// supabase/functions/webhook/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";
async function verifyPaystackSignature(
body: string,
signature: string,
secretKey: string
): Promise<boolean> {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secretKey),
{ name: "HMAC", hash: "SHA-512" },
false,
["sign"]
);
const sig = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(body)
);
const hash = Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hash === signature;
}
serve(async (req) => {
if (req.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const signature = req.headers.get("x-paystack-signature") ?? "";
const secretKey = Deno.env.get("PAYSTACK_SECRET_KEY") ?? "";
// Read raw body before parsing
const body = await req.text();
const valid = await verifyPaystackSignature(body, signature, secretKey);
if (!valid) {
return new Response("Invalid signature", { status: 400 });
}
// Return 200 immediately — Paystack retries on slow responses
const response = new Response("ok", { status: 200 });
// Parse and process after returning
const event = JSON.parse(body);
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Idempotency: skip if already processed
const { data: existing } = await supabase
.from("webhook_events")
.select("id")
.eq("event_id", event.data?.id)
.maybeSingle();
if (existing) return response;
if (event.event === "charge.success") {
const reference = event.data.reference;
const amount = event.data.amount / 100;
await supabase
.from("orders")
.update({
status: "paid",
paid_at: new Date().toISOString(),
amount_paid: amount,
})
.eq("reference", reference);
}
if (event.event === "transfer.success") {
const transferCode = event.data.transfer_code;
await supabase
.from("transfers")
.update({ status: "success" })
.eq("transfer_code", transferCode);
}
// Record processed event
await supabase.from("webhook_events").insert({
event_id: event.data?.id,
event_type: event.event,
processed_at: new Date().toISOString(),
});
return response;
});
# Create the function
supabase functions new webhook
# Set your Paystack secret key
supabase secrets set PAYSTACK_SECRET_KEY=sk_live_xxxxxxxxxxxx
# Deploy
supabase functions deploy webhook
After deploying, set the webhook URL in your Paystack dashboard:
https://<your-project-ref>.supabase.co/functions/v1/webhook
Create the idempotency table in Supabase:
create table webhook_events (
id uuid default gen_random_uuid() primary key,
event_id text unique not null,
event_type text not null,
processed_at timestamptz not null
);
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 Supabase Edge 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