🛠️ Developer8 min read·September 9, 2026
How to Transform Stripe Webhook Payloads Without Writing Boilerplate
Stripe webhooks arrive as deeply nested JSON. Getting from the raw payload to the fields you actually need — customer email, amount charged, subscription status — requires extracting and transforming the data. Here's the practical guide.
The Stripe webhook payload structure
Every Stripe event has the same outer shape. The type field tells you what happened. The data.object contains the relevant Stripe object.
{
"id": "evt_1OsZQH...",
"type": "checkout.session.completed", // ← what happened
"data": {
"object": {
// ← the Stripe object (Session, Invoice, Subscription, etc.)
"amount_total": 4999,
"customer_email": "user@example.com",
"payment_status": "paid"
}
}
}The 12 most common event types
checkout.session.completed— one-time payment succeededinvoice.payment_succeeded— subscription renewal paidinvoice.payment_failed— subscription renewal failedcustomer.subscription.created— new subscription startedcustomer.subscription.updated— plan changed or trial endedcustomer.subscription.deleted— subscription cancelledpayment_intent.succeeded— payment confirmedpayment_intent.payment_failed— payment declinedcustomer.created— new Stripe customer recordcharge.refunded— full or partial refund issuedcharge.dispute.created— chargeback filedproduct.created/price.created— catalog updates
A type-safe webhook handler
import Stripe from "stripe"
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")!
// Verify the webhook signature
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return new Response("Invalid signature", { status: 400 })
}
// Route by event type
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object
await fulfillOrder({
email: session.customer_email!,
amount: session.amount_total!,
metadata: session.metadata,
})
break
}
case "invoice.payment_failed": {
const invoice = event.data.object
await notifyPaymentFailed(invoice.customer_email!)
break
}
}
return new Response("ok", { status: 200 })
}Common payload transformations
- Amount cents → dollars:
amount_total / 100(Stripe always sends amounts in the smallest currency unit) - Unix timestamp → Date:
new Date(event.created * 1000) - Metadata extraction:
session.metadata?.userId— always check for null/undefined - Currency formatting: use
Intl.NumberFormatwith thecurrencyfield from the payload
Working with non-Stripe webhooks too? Aarunya Webhook Transformer shows the payload structure for Stripe, GitHub, Shopify, Twilio, and more — with transformation code examples for each.
Try the related tool
Webhook Transformer — free, runs 100% in your browser.
Open Webhook Transformer →Enjoyed this? Get notified when Pro launches.
