Webhooks Deep Dive
Webhooks are how services talk to each other in real-time. When something happens in one system, it sends an HTTP POST to your endpoint. No polling. No delays. This is the nervous system of your AI stack.
Polling vs. Webhooks: Why It Matters
There are two ways for your app to learn that something happened in an external service. The difference between them is the difference between checking your mailbox every 5 minutes and having the mail carrier ring your doorbell.
Your app asks "did anything happen?" every N seconds. Wastes API calls, introduces delays, and burns through rate limits. If you poll every 30 seconds, a payment could happen at second 1 and you would not know until second 30.
The external service pushes a notification to YOUR endpoint the moment something happens. Zero wasted calls, instant notification, no rate limit concerns. Stripe fires a webhook within milliseconds of a payment completing.
How a Webhook Request Works
When an event occurs, the external service sends an HTTP POST request to a URL you registered. The request contains a JSON payload describing the event. Your endpoint processes it and responds with a 200 OK.
/functions/v1/stripe-webhookConfiguring a Stripe Webhook
Setting up your first webhook takes 5 minutes in the Stripe Dashboard. Here is exactly what to configure and why.
# Go to: Stripe Dashboard → Developers → Webhooks → "Add endpoint"
Endpoint URL:
https://yourproject.supabase.co/functions/v1/stripe-webhook
Events to subscribe:
checkout.session.completed # Someone paid — grant access
payment_intent.succeeded # Payment confirmed
payment_intent.failed # Payment failed — alert team
customer.subscription.created # New subscription
customer.subscription.deleted # Cancellation — trigger retention flow
invoice.paid # Recurring payment successful
invoice.payment_failed # Failed recurring — alert + retry
charge.refunded # Refund processed — revoke access
Signing Secret:
whsec_... # Stripe provides this — save as env var
# Store in Supabase: STRIPE_WEBHOOK_SECRET
# NEVER put this in code or commit it to git
checkout.session.completed and charge.refunded to start. Add more events as your business logic grows. Every event you subscribe to is a webhook your endpoint must handle — do not subscribe to events you are not ready to process.
Signature Verification: The Security Gate
Anyone can send a POST to your endpoint. A bad actor could fake a "payment succeeded" event to get free access. Stripe prevents this by cryptographically signing every webhook with a secret only you and Stripe know.
Always verify the signature. No exceptions. Here is the complete verification flow for a Supabase Edge Function:
import Stripe from "https://esm.sh/stripe@14"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
// Initialize Stripe with your secret key
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!)
// Initialize Supabase with service role (server-side only)
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
)
Deno.serve(async (req) => {
// Step 1: Read the RAW body — must be text, not parsed JSON
// Why? The signature is computed on exact bytes. Parsing
// changes whitespace/ordering and invalidates the signature.
const body = await req.text()
const sig = req.headers.get("stripe-signature")!
// Step 2: Verify the signature — THE security check
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body, sig, Deno.env.get("STRIPE_WEBHOOK_SECRET")!
)
} catch (err) {
// Signature invalid — reject immediately
return new Response("Invalid signature", { status: 400 })
}
// Step 3: Handle the event — only after verification passes
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session
await supabase.from("revenue").insert({
amount: session.amount_total,
customer_email: session.customer_details?.email,
stripe_session_id: session.id,
})
}
// Step 4: Respond 200 immediately — Stripe expects this fast
return new Response(JSON.stringify({ received: true }))
})
req.text() — never req.json() — before signature verification. The signature is an HMAC computed on the exact raw bytes of the request body. Parsing the JSON first can change whitespace or key ordering, producing a different byte sequence and causing verification to fail silently.
This lesson is for Pro members
Unlock all 520+ lessons across 52 courses with Academy Pro.
Already a member? Sign in to access your lessons.