Stripe payment notifications on your phone
You shipped a product. Someone is paying. The gap between “payment processed” and “you knowing about it” is however long until you remember to open the Stripe dashboard.
Here’s how to close that gap.
The three-line version
Install the package:
npm install trigger.fyiAdd it after your Stripe webhook handler:
import fyi from "trigger.fyi"
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"], process.env.STRIPE_WEBHOOK_SECRET)
if (event.type === "payment_intent.succeeded") {
const amount = (event.data.object.amount / 100).toFixed(2)
const currency = event.data.object.currency.toUpperCase()
res.json({ received: true })
fyi(`${currency} ${amount} payment`, { body: event.data.object.receipt_email })
}
res.json({ received: true })
})That’s it. TRIGGER_FYI_SECRET_KEY in your environment, one fyi() call after the response is sent, and your phone knows every time someone pays.
Why after the response
Stripe expects a 200 within a few seconds or it retries. fyi() is fire-and-forget — it never throws, never blocks — but you still want the response sent first. On most runtimes, un-awaited promises survive. On serverless (Vercel, Cloudflare Workers), they might not. The safe pattern:
// Option A: after res.json(), works on traditional servers
res.json({ received: true })
fyi(`Payment: $${amount}`)
// Option B: await it — one edge roundtrip, can't throw, safe everywhere
res.json({ received: true })
await fyi(`Payment: $${amount}`)
// Option C: Cloudflare Workers
ctx.waitUntil(fyi(`Payment: $${amount}`))What to put in the notification
The first argument is the title — what shows in bold on the lock screen. The second adds detail.
// Minimal
fyi(`$${amount} payment`)
// With subscriber info
fyi(`$${amount} payment`, { body: customer.email })
// With plan context
fyi(`$${amount} payment`, {
body: customer.email,
plan: subscription.items.data[0].price.nickname,
interval: subscription.items.data[0].price.recurring.interval
})Anything in the second argument beyond body becomes filterable metadata. Open the feed, filter by plan: pro, see only pro payments.
Events worth notifying on
Beyond payment_intent.succeeded, a few others are worth wiring up:
switch (event.type) {
case "payment_intent.succeeded":
fyi(`Payment: $${amount}`, { body: customer.email })
break
case "invoice.payment_failed":
fyi.critical(`Failed payment: $${amount}`, { body: customer.email })
break
case "customer.subscription.deleted":
fyi.log(`Cancellation`, { body: customer.email, plan: plan.nickname })
break
case "customer.subscription.created":
fyi(`New subscriber`, { body: customer.email, plan: plan.nickname })
break
}fyi() pushes to your phone. fyi.critical() uses Web Push urgency high — the OS treats it with higher priority. fyi.log() records in the feed without a push — cancellations are worth tracking, not worth interrupting.
Getting your phone set up
If you haven’t set up trigger.fyi:
npx trigger.fyiThis generates a key, shows a QR code, and opens a live feed in your terminal. Scan the QR on your phone to subscribe that device. Then paste your key into your environment:
TRIGGER_FYI_SECRET_KEY=your_key_here
The feed and phone receive the same events. The feed is the log. The phone is the interrupt.
One honest note on iOS
iOS web push can’t break silent mode. fyi.critical() arrives, but it won’t override Do Not Disturb on an iPhone the way Android will. That’s a platform constraint. The docs say so. If you need to be woken up for failed payments, Android or a native app is the reliable path for critical-urgency delivery.
For most Stripe events — new subscribers, successful payments, plan upgrades — normal priority is exactly right.