Aarunya AppsAarunya Apps
🛠️ 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 succeeded
  • invoice.payment_succeeded — subscription renewal paid
  • invoice.payment_failed — subscription renewal failed
  • customer.subscription.created — new subscription started
  • customer.subscription.updated — plan changed or trial ended
  • customer.subscription.deleted — subscription cancelled
  • payment_intent.succeeded — payment confirmed
  • payment_intent.payment_failed — payment declined
  • customer.created — new Stripe customer record
  • charge.refunded — full or partial refund issued
  • charge.dispute.created — chargeback filed
  • product.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.NumberFormat with the currency field 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.