📚Academy
likeone
online

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.

Polling (The Old Way)

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.

Webhooks (The Right Way)

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.

1. Event occurs — user completes payment on Stripe
2. Stripe sends HTTP POST — JSON payload with event type, data, and cryptographic signature
3. Your endpoint receives it — a Supabase Edge Function at /functions/v1/stripe-webhook
4. Verify signature — confirm the request genuinely came from Stripe, not a bad actor
5. Process the event — insert revenue row, grant access, send notification
6. Respond 200 OK — tell Stripe you received and handled it (must respond within 20 seconds)

Configuring a Stripe Webhook

Setting up your first webhook takes 5 minutes in the Stripe Dashboard. Here is exactly what to configure and why.

Stripe Dashboard — Webhook configuration
# 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
Start small: For a course or product site, you only need 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:

TypeScript — Supabase Edge Function: stripe-webhook
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 }))
})
Critical: You MUST use 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.

Academy
Built with soul — likeone.ai