Transform Stripe Webhook Payloads
Time: ~10 minutes | Difficulty: Beginner | Prerequisites: Stripe account with webhook access, Slashbin account
TL;DR: Stripe webhooks arrive as deeply nested envelopes. Instead of writing parsing code, define a Golden Model visually in Slashbin — map the fields you care about, cast currency safely, and your endpoint receives clean flat JSON with the schema you designed.
What You'll Build
By the end of this guide, your database consumer will receive clean, flat JSON like this — a Postgres-bound service can INSERT it directly, no envelope-walking required:
{
"invoice_id": "in_1MtwBwLkdIwHu7ix4LGN3vfB",
"customer_id": "cus_NcTh4G8kxKPeUM",
"subscription_id": "sub_1MtwBqLkdIwHu7ixEo1YRZg8",
"amount": 49.00,
"currency": "usd",
"status": "paid",
"paid_at": "2023-04-04T18:30:00Z",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_1.../test_YWNjdF8x..."
}Instead of parsing nested data.object.* in your handler, you define the schema once in the Transformation IDE and Slashbin extracts, casts, validates, and delivers only the fields your API expects.
The Problem
Stripe events arrive wrapped in an envelope: { id, type, created, data: { object: { ... } } }. The fields you actually need are buried under data.object — and worse, currency amounts come as integer cents in strings that will float-error if you JSON.parse and multiply.
Without a transform layer, your handler is on the hook for:
| Without Slashbin | With Slashbin |
|---|---|
Walk event.data.object.customer in every handler | Map once visually, receive customer_id at the top level |
| Convert integer cents ↔ decimal amounts by hand | Safe decimal casting on currency fields at the gateway |
| Rewrite handlers when Stripe adds fields you don't need | Golden Model ignores unmapped source fields automatically |
Fail silently when a required field arrives null | Required-field toggles reject invalid payloads before delivery |
The Solution
Define a Golden Model in Slashbin's Transformation IDE. Visually map nested Stripe fields to a flat schema, toggle validation on the critical ones, and Slashbin transforms every incoming event before it reaches your endpoint.
Outcome: Your backend receives the exact JSON shape you designed. Bad data is rejected at the gateway, not in your application.
Step 1: Create a Stripe Project in Slashbin
Secure the connection before any data flows.
- Project Setup: Click Create New Project and select Stripe.
- Type: Choose Transactional — Stripe events are stateful business events.
- Security: Paste your Stripe Webhook Signing Secret (starts with
whsec_, available in Stripe Dashboard → Developers → Webhooks → your endpoint → Signing secret).
Result: Slashbin auto-rejects any request that fails Stripe's HMAC signature check, so only verified Stripe events reach your transform pipeline.
Step 2: Build the Golden Model in the Transformation IDE
Open the Transformation IDE. The raw Stripe event sits on the left ("Source Data"); your clean target schema is on the right.
Map the Fields You Care About
Use the Field Mappings panel to define the flat JSON your consumer expects:
- + Add — create a new field mapping. Enter the target field name (e.g.,
invoice_id) and set its JSONPath source (e.g.,$.data.object.id). - Pencil icon — edit an existing mapping to change its source path or validation.
- Add All — import all source fields as-is when you need a full pass-through for debugging.
For an invoice.paid event, the mappings that produce the clean payload above look like this:
| Target field | Source JSONPath | Notes |
|---|---|---|
invoice_id | $.data.object.id | Required |
customer_id | $.data.object.customer | Required |
subscription_id | $.data.object.subscription | Optional |
amount | $.data.object.amount_paid | Decimal cast — cents → dollars |
currency | $.data.object.currency | Required |
status | $.data.object.status | Required |
paid_at | $.created | ISO timestamp from Unix seconds |
hosted_invoice_url | $.data.object.hosted_invoice_url | Optional |
Pro tip: Toggle Required ON for invoice_id, customer_id, amount, currency, and status. If any is missing, Slashbin flags the event as invalid instead of forwarding a half-formed row to your database.
Cast Currency Safely
Stripe sends money as integer cents (4900 = $49.00). The Transformation IDE supports decimal-safe casting on numeric fields so amount_paid arrives at your endpoint as 49.00, not 4900 or 49.000000001 — no parseFloat traps in your handler.
For fields where the shape depends on the event (e.g., mapping data.object.subscription to a nullable string when Stripe omits it on one-off invoices), use a Derived Mapping to encode the rule declaratively instead of branching in your consumer.
Step 3: Publish the Model
Before Slashbin transforms live events, publish the model.
Click Publish in the top navigation to activate it. The version number increments on each publish.
Safe by default: Unpublished models don't drop events. Incoming Stripe webhooks are captured in the Dead Letter Queue until a matching model is live, so nothing is lost while you iterate.
Step 4: Deliver the Clean Payload to Your Destination
On the Outbound tab, click + Add Destination and choose Custom Webhook.
- Enter a Name (e.g., "Postgres Ingest").
- Enter your URL (e.g.,
https://api.example.com/webhooks/stripe-invoices). - Add any headers your endpoint requires, then click Add Destination.
The Developer Guide tab shows the exact JSON your endpoint will receive plus signature-verified receiver code in multiple languages. Your handler now inserts a single flat row per event — no envelope walking:
// Express — consume the transformed payload directly.
import express from 'express'
const app = express()
app.post('/webhooks/stripe-invoices', express.json(), async (req, res) => {
const invoice = req.body // already flat: { invoice_id, customer_id, amount, ... }
await db.query(
`INSERT INTO invoices (invoice_id, customer_id, subscription_id, amount, currency, status, paid_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (invoice_id) DO UPDATE SET status = EXCLUDED.status, amount = EXCLUDED.amount`,
[
invoice.invoice_id,
invoice.customer_id,
invoice.subscription_id,
invoice.amount,
invoice.currency,
invoice.status,
invoice.paid_at,
],
)
res.status(200).end()
})Copy the Ingestion URL from the Slashbin dashboard and paste it into Stripe Dashboard → Developers → Webhooks. Select the event types you want transformed (e.g., invoice.paid, invoice.payment_failed, checkout.session.completed).
Done. Stripe posts to Slashbin, the Golden Model transforms every event, and your endpoint receives clean flat JSON on every delivery.
Before & After
What Stripe Sends (nested envelope):
{
"id": "evt_1MtwBwLkdIwHu7ixV1YrJ8vN",
"object": "event",
"type": "invoice.paid",
"created": 1680640200,
"data": {
"object": {
"id": "in_1MtwBwLkdIwHu7ix4LGN3vfB",
"object": "invoice",
"customer": "cus_NcTh4G8kxKPeUM",
"subscription": "sub_1MtwBqLkdIwHu7ixEo1YRZg8",
"amount_paid": 4900,
"amount_due": 4900,
"currency": "usd",
"status": "paid",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_1.../test_YWNjdF8x...",
"lines": { "data": [ /* nested line items */ ] },
"...": "60+ more fields (period, discounts, tax, metadata, etc.)"
}
}
}What Your API Receives (Golden Model):
{
"invoice_id": "in_1MtwBwLkdIwHu7ix4LGN3vfB",
"customer_id": "cus_NcTh4G8kxKPeUM",
"subscription_id": "sub_1MtwBqLkdIwHu7ixEo1YRZg8",
"amount": 49.00,
"currency": "usd",
"status": "paid",
"paid_at": "2023-04-04T18:30:00Z",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_1.../test_YWNjdF8x..."
}Why Slashbin vs a Raw Handler
- Schema enforced once, not per handler. Golden Model defines the shape at the gateway. Every downstream service sees the same JSON.
- Decimal-safe currency casting. Cents → dollars is a mapping property, not a
parseFloatyou have to remember in every language. - No parsing code to maintain. When Stripe adds a field, your handler doesn't change — the model already ignores unmapped source fields.
- Retries and replay for free. Because the transform runs at the gateway, retries and replay operate on the original raw payload — you can re-run any event through the latest model without touching Stripe.
Next Steps
- Read Golden Model for the full validation and derived-field surface.
- Guarantee delivery survives a bad deploy — see Retry Stripe Webhooks Reliably.
- Explore Replay & Retry to re-run the same Stripe event through an updated model.
- Browse every supported source and destination on Integrations.