Payment received notifications
A payment comes in. You find out when you remember to check the dashboard.
Here’s how to know immediately instead.
Stripe
import Stripe from "stripe"
import fyi from "trigger.fyi"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
export async function POST(req) {
const body = await req.text()
const sig = req.headers.get("stripe-signature")
let event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET)
} catch {
return new Response("Bad signature", { status: 400 })
}
const response = new Response("OK")
switch (event.type) {
case "payment_intent.succeeded": {
const pi = event.data.object
const amount = (pi.amount / 100).toFixed(2)
const currency = pi.currency.toUpperCase()
fyi(`${currency} ${amount} received`, { body: pi.receipt_email })
break
}
case "invoice.paid": {
const inv = event.data.object
const amount = (inv.amount_paid / 100).toFixed(2)
fyi(`Subscription payment: $${amount}`, {
body: inv.customer_email,
plan: inv.lines.data[0]?.description
})
break
}
case "invoice.payment_failed": {
const inv = event.data.object
fyi.critical("Payment failed", { body: inv.customer_email })
break
}
}
return response
}Paddle
import fyi from "trigger.fyi"
import crypto from "crypto"
export async function POST(req) {
const body = await req.text()
const signature = req.headers.get("paddle-signature")
// Verify Paddle webhook signature
const ts = signature.match(/ts=(\d+)/)?.[1]
const h1 = signature.match(/h1=([a-f0-9]+)/)?.[1]
const hmac = crypto.createHmac("sha256", process.env.PADDLE_WEBHOOK_SECRET)
hmac.update(`${ts}:${body}`)
if (hmac.digest("hex") !== h1) return new Response("Bad signature", { status: 400 })
const event = JSON.parse(body)
const response = new Response("OK")
if (event.event_type === "transaction.completed") {
const amount = event.data.details.totals.total
const currency = event.data.currency_code
const email = event.data.customer?.email
fyi(`${currency} ${(amount / 100).toFixed(2)} received`, { body: email })
}
return response
}Lemon Squeezy
import fyi from "trigger.fyi"
import crypto from "crypto"
export async function POST(req) {
const body = await req.text()
const signature = req.headers.get("x-signature")
const digest = crypto.createHmac("sha256", process.env.LEMONSQUEEZY_WEBHOOK_SECRET)
.update(body).digest("hex")
if (digest !== signature) return new Response("Bad signature", { status: 400 })
const event = JSON.parse(body)
const response = new Response("OK")
if (event.meta.event_name === "order_created") {
const order = event.data.attributes
fyi(`$${order.total_formatted} received`, {
body: order.user_email,
product: order.first_order_item.product_name
})
}
return response
}What to include
The first argument is the bold line on your lock screen. Keep it short and specific:
// Too generic
fyi("Payment received")
// Better
fyi("$49 received", { body: "[email protected]" })
// With context
fyi("$49/mo · Pro", { body: "[email protected]", country: "US" })The metadata fields are filterable. Open the feed and filter by country: US to see only US payments, or plan: pro for pro subscribers.
Failed payments deserve critical
fyi.critical("Payment failed", {
body: customer.email,
amount: `$${amount}`,
attempt: event.data.object.attempt_count
})fyi.critical() uses Web Push urgency high. On iOS, it arrives at normal priority — iOS web push can’t override silent mode. On Android, urgency high is honored.
Setup
npx trigger.fyiGenerates a key and subscribes your device. Add to your environment:
TRIGGER_FYI_SECRET_KEY=your_key_here
STRIPE_WEBHOOK_SECRET=whsec_...
The key is the channel. Use a different key if you want separate notification streams for different payment providers.
Related: Stripe payment notifications · Get notified when someone signs up · Push notifications from Node.js · What is trigger.fyi?