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.
The receiver
Section titled “The receiver”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 keyconst 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.
Set it up
Section titled “Set it up”- Remix this template — click the Remix button in the top right corner.
- In the Stripe dashboard,
add a webhook endpoint pointing at your val's HTTP URL (shown on the HTTP
trigger of
main.ts). - Copy that endpoint's signing secret (it starts with
whsec_) and add it to your val's environment variables asSTRIPE_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 aftersaveEvent.
Next steps
Section titled “Next steps”- Stripe webhooks guide — payment links, Stripe Checkout, and fulfillment
- Creating a webhook — webhooks on Val Town in general
- SQLite reference — querying your stored events