Use case
Stripe webhooks in under 50 lines.
Stripe sends you payment events. You need to turn them into database writes. A Stripe webhook handler on nvoke is one function, one URL, one secret.
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function (req) {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch {
return new Response("bad signature", { status: 400 });
}
switch (event.type) {
case "checkout.session.completed":
await markOrderPaid(event.data.object);
break;
case "customer.subscription.deleted":
await markSubscriptionCanceled(event.data.object);
break;
}
return { received: true };
}Setup, end to end
Create a function on nvoke, paste the handler above, add two secrets: STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET. Mark the endpoint as public so Stripe can POST without an API key header. Copy the endpoint URL, open the Stripe dashboard, add it as a webhook endpoint, select the events you care about. Stripe will send a test event — you will see it hit in the log panel within a second.
That is the entire setup. No Vercel project, no Lambda + API Gateway, no ngrok tunnel during development. Local development, if you want it, is a matter of running stripe listen --forward-to against the same URL.
Idempotency, the quiet killer
Stripe will retry a webhook on any non-2xx response, and will sometimes deliver the same event twice for other reasons. Every handler must be idempotent: handling checkout.session.completed twice for the same session must not charge the customer again or send two confirmation emails.
The usual pattern: use event.id as a dedupe key. Either skip handling if you have already seen the ID, or make the downstream side effect idempotent (UPSERT, a unique constraint on event_id in the order table). Both work; the second is more robust under concurrent delivery.
What not to do in a webhook
Do not block the webhook response on slow downstream work. If you need to send a complex email, reconcile against an external system, or do anything that might take more than a few seconds, write the event to a database table and process it from a scheduled nvoke function. Stripe wants a fast 200.
Signature verification built-in
req.rawBody is the untouched request body — exactly what Stripe signed. Pass it to stripe.webhooks.constructEvent and you are done.
One endpoint, many events
Stripe encourages a single endpoint for all events. Route by event.type inside the function. Keep everything in one editable file.
Replay the exact payload
When something goes wrong in production, replay the captured invocation with the same signed payload. No dry-run harness to build.
Ship a Stripe handler tonight.
Paste the handler above, add your keys, copy the URL into Stripe. Done before dinner.