App Finishers

Fix guide · updated 2026-11-07

Stripe webhook secret: where to find it, and fixing 400/401 errors

No signatures found matching the expected signature for payload

A customer pays. Stripe shows the payment. But your app never marks the order as paid, because the webhook (the message Stripe sends to your app after each payment) keeps failing. In Stripe's dashboard (Developers → Webhooks → your endpoint), the attempts show a 400 or a 401. Here's what each means and how to fix it.

Where to find your webhook secret

Stripe dashboard → Developers → Webhooks (it opens in Workbench) → click the endpoint your app uses → Signing secret → Reveal. It starts with whsec_. Test mode and live mode each have their own endpoints and secrets, so copy it in the mode your app runs in. This is not your API key: the API key starts with sk_.

A 401: Supabase blocks Stripe at the door

Supabase Edge Functions only accept requests from signed-in users by default. Stripe isn't one of your users, so Supabase refuses it with 401 Missing authorization header before your code even runs.

Fix: switch off that check for the webhook function only. Either deploy it with:

supabase functions deploy stripe-webhook --no-verify-jwt

or add this to supabase/config.toml and redeploy:

[functions.stripe-webhook]
verify_jwt = false

This is safe because your function checks Stripe's own signature instead (below). Without that signature check, anyone could send fake "payment succeeded" messages.

A 400: the signature check fails

The function runs, but rejects the message:

No signatures found matching the expected signature for payload

Stripe signs every webhook with a secret that starts with whsec_. Your function recomputes the signature and compares. Three things break it:

1. The wrong secret

Each endpoint has its own secret, and test mode and live mode have different ones. The Stripe CLI (stripe listen) prints yet another one. A secret copied from the wrong place fails every time.

Fix: Stripe dashboard → Developers → Webhooks → the exact endpoint your app uses, in the right mode → Signing secret → reveal and copy. Then save it on Supabase:

supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...

2. The message was changed before the check

The signature covers the message exactly as Stripe sent it, byte for byte. If your code reads it with req.json() first, the text gets rebuilt and no longer matches.

Fix: read the raw text with req.text() and verify that.

3. The synchronous check doesn't work here

Supabase Edge Functions run on Deno, where Stripe's usual constructEvent() can't do the cryptography. You need the async version with Web Crypto.

A webhook function that works

import Stripe from "npm:stripe@17";

const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
const cryptoProvider = Stripe.createSubtleCryptoProvider();

Deno.serve(async (req) => {
  const signature = req.headers.get("stripe-signature");
  const body = await req.text(); // raw text, never req.json() first

  let event: Stripe.Event;
  try {
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature!,
      Deno.env.get("STRIPE_WEBHOOK_SECRET")!,
      undefined,
      cryptoProvider,
    );
  } catch (err) {
    return new Response(`Webhook signature failed: ${(err as Error).message}`, { status: 400 });
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    // mark the order as paid here, using session.id or session.metadata
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
});

Deploy it with --no-verify-jwt, then click Resend on a failed event in Stripe's dashboard and watch it turn green.

Still not working?

Look at the Response of a failed attempt in Stripe's dashboard and at Edge Functions → Logs in Supabase: together they show which step fails. If the webhook works but orders still don't update, the problem has moved to your database rules, often the one in our row-level security guide. Payments are where a stuck app costs real money, so they're the fixes we do fastest.