Skip to content

Host a Stripe webhook receiver

A Stripe webhook receiver is just an HTTPS endpoint, but it has to be deployed somewhere public before Stripe will deliver to it — which makes a plain HTTP val a natural home for one. This template is a ready-made receiver: it verifies the Stripe-Signature header against your webhook signing secret, stores each event in the val's SQLite database, and returns 200 immediately.

If you're starting from the payments side — payment links, Stripe Checkout, and what a fulfillment webhook is for — read the Stripe webhooks guide first. This page is just the hosted-receiver part.

View and run this example on Val Town

import Stripe from "npm:stripe";
import { saveEvent } from "./db.ts";
// Signature verification only needs the webhook secret, not an API key
const stripe = new Stripe("sk_not_used_for_webhook_verification");
export default async function (req: Request): Promise<Response> {
if (req.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const secret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
if (!secret) {
return new Response("Missing STRIPE_WEBHOOK_SECRET env var", { status: 500 });
}
const signature = req.headers.get("Stripe-Signature");
if (!signature) {
return new Response("Missing Stripe-Signature header", { status: 400 });
}
const body = await req.text();
let event: Stripe.Event;
try {
event = await stripe.webhooks.constructEventAsync(body, signature, secret);
} catch (err) {
return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 });
}
await saveEvent(event, body);
return Response.json({ received: true });
}

The saveEvent helper in db.ts creates a stripe_events table (id, type, created, full event JSON) and ignores duplicate deliveries of the same event id, so Stripe's at-least-once delivery doesn't create duplicate rows.

  1. Remix this template — click the Remix button in the top right corner.
  2. In the Stripe dashboard, add a webhook endpoint pointing at your val's HTTP URL (shown on the HTTP trigger of main.ts).
  3. Copy that endpoint's signing secret (it starts with whsec_) and add it to your val's environment variables as STRIPE_WEBHOOK_SECRET.

To test it, configure your endpoint in Stripe test mode and run stripe trigger payment_intent.succeeded with the Stripe CLI. Expect {"received":true} and a new row in stripe_events — the val's README also shows how to forge a correctly signed test event with curl. Requests with a missing or bad signature get a 400.

  • Signature verification only needs the webhook secret — no Stripe API key is required to receive events.
  • Each remix gets its own private SQLite database, so your event log stays with your copy. Browse it in the val's SQLite tab.
  • The receiver stores events and returns immediately. Do fulfillment work (sending emails, updating your database) by querying stripe_events, or add it to the handler after saveEvent.