Forward Stripe webhooks to your phone
Stripe fires a webhook. You find out when you check the dashboard.
Here’s a complete guide to turning every Stripe webhook into a phone notification — with signature verification, proper serverless handling, and sensible defaults.
The complete handler
import Stripe from "stripe"
import { waitUntil } from "@vercel/functions"
import fyi from "trigger.fyi"
import { NextResponse } from "next/server"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get("stripe-signature")!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch (err) {
return NextResponse.json({ error: "Webhook signature verification failed" }, { status: 400 })
}
const response = NextResponse.json({ received: true })
waitUntil(handleEvent(event))
return response
}
async function handleEvent(event: Stripe.Event) {
switch (event.type) {
case "payment_intent.succeeded":
await onPayment(event.data.object as Stripe.PaymentIntent)
break
case "invoice.paid":
await onInvoicePaid(event.data.object as Stripe.Invoice)
break
case "invoice.payment_failed":
await onPaymentFailed(event.data.object as Stripe.Invoice)
break
case "customer.subscription.created":
await onSubscriptionCreated(event.data.object as Stripe.Subscription)
break
case "customer.subscription.deleted":
await onCancellation(event.data.object as Stripe.Subscription)
break
}
}Per-event handlers
async function onPayment(pi: Stripe.PaymentIntent) {
const amount = (pi.amount / 100).toFixed(2)
const currency = pi.currency.toUpperCase()
await fyi(`${currency} ${amount} payment`, {
body: pi.receipt_email ?? undefined,
intent_id: pi.id
})
}
async function onInvoicePaid(invoice: Stripe.Invoice) {
const amount = (invoice.amount_paid / 100).toFixed(2)
const plan = invoice.lines.data[0]?.description ?? "subscription"
await fyi(`$${amount} subscription payment`, {
body: invoice.customer_email ?? undefined,
plan
})
}
async function onPaymentFailed(invoice: Stripe.Invoice) {
const amount = (invoice.amount_due / 100).toFixed(2)
await fyi.critical(`Payment failed: $${amount}`, {
body: invoice.customer_email ?? undefined,
attempt: invoice.attempt_count
})
}
async function onSubscriptionCreated(sub: Stripe.Subscription) {
const plan = sub.items.data[0]?.price.nickname ?? "subscription"
await fyi(`New subscriber: ${plan}`, {
customer_id: sub.customer as string
})
}
async function onCancellation(sub: Stripe.Subscription) {
const plan = sub.items.data[0]?.price.nickname ?? "subscription"
await fyi.log(`Cancellation: ${plan}`, {
customer_id: sub.customer as string,
reason: sub.cancellation_details?.reason ?? undefined
})
}Cancellations use fyi.log() — recorded in the feed without a push. You want to know about them, but not be interrupted every time.
Setting up the Stripe webhook
- In Stripe Dashboard: Developers → Webhooks → Add endpoint
- URL:
https://your-domain.com/api/webhooks/stripe - Events to listen to:
payment_intent.succeededinvoice.paidinvoice.payment_failedcustomer.subscription.createdcustomer.subscription.deleted
- Copy the Signing secret →
STRIPE_WEBHOOK_SECRET
For local development, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripeThis gives you a local webhook secret and forwards events to your dev server.
Environment variables
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
TRIGGER_FYI_SECRET_KEY=your_key_here
Testing
Send a test event from the Stripe Dashboard (Webhooks → your endpoint → Send test webhook) or via CLI:
stripe trigger payment_intent.succeededYour phone should notify within a few seconds.
Filtering in the feed
Open npx trigger.fyi and filter by metadata. All the handlers above attach metadata:
plan: pro— see only pro subscription eventsattempt: 2— see only second-attempt payment failures
The feed keeps 30 days of history. Good for diagnosing payment issues after the fact.
Related: Stripe payment notifications · Webhook notifications to your phone · Push notifications from Cloudflare Workers