How to inspect webhook payloads in an interactive json tree
- Step 1Capture a sample payload — Use the provider's test/sample event: Stripe CLI
stripe trigger checkout.session.completed(or the Dashboard's sample events), GitHub's webhook 'Recent Deliveries' → copy the request body, Shopify's webhook test send, or a captured request from a tunnel (ngrok, smee). Save the JSON body to a.jsonfile. - Step 2Drop the file onto the viewer — Accepted extensions:
.json,.ndjson,.jsonl,.txt. Use a.jsonfile with one event body. The free tier handles files up to 2 MB — far more than any single webhook payload needs. - Step 3Leave Max depth at the default — Webhook payloads rarely exceed a handful of levels, so the default Max depth of 20 builds the full tree. If you only want the envelope of an unusually deep payload, lower it — deeper blocks still appear as collapsed
Object(n)nodes. - Step 4Click View Tree and read the event type first — Find the discriminator: Stripe uses
type(checkout.session.completed), GitHub uses anactionfield plus the event header, Shopify/Slack vary. Confirm it, then expand intodata.object(Stripe) or the relevant body. The toolbar shows node count and max depth. - Step 5Search for the field your handler needs — Type
email,amount,status, or the value you're matching. The matching branch auto-expands and the rest collapse, so you immediately see where the field lives in the payload. - Step 6Copy the path into your handler — Hover the field and click Copy to grab
$.data.object.customer_details.email. Paste it into your handler's accessor, a json-path-extractor test, or your event-mapping notes. Download exports the tree shape as<name>.tree.jsonfor sharing with teammates.
Where the field you need lives, by provider
Common webhook envelopes and the paths handlers most often reach for. Paths in the viewer's $ / .key / [n] notation. Field names reflect typical payloads; always confirm against your captured sample.
| Provider | Event discriminator | Body lives under | Example useful path |
|---|---|---|---|
| Stripe | $.type | $.data.object | $.data.object.customer_details.email |
| GitHub | $.action + X-GitHub-Event header | top level ($.pull_request, $.issue) | $.pull_request.head.ref |
| Shopify | X-Shopify-Topic header | top level (order/product body) | $.line_items[0].sku |
| Slack (Events API) | $.type / $.event.type | $.event | $.event.text |
| Generic / custom | varies (event, eventType) | $.data / $.payload | $.data.id |
Type gotchas in webhook payloads
What the node type reveals about values that are easy to mishandle in a handler.
| Field example | Node type | Renders as | Why it matters |
|---|---|---|---|
Stripe amount_total | number | 4200 | Integer cents, not dollars — 4200 = $42.00 |
livemode | boolean | true / false | Real boolean; don't compare against the string "true" |
id (evt_..., pi_...) | string | "evt_1A..." | Prefixed string IDs — never numeric, no precision risk |
metadata | object | Object(n) | Free-form key/value; counts show whether any was set |
line_items / commits | array | Array(n) | Confirms the array came back populated vs Array(0) |
| optional field | null | null | Present-and-null differs from an omitted key in your handler |
Cookbook
Webhook-inspection walkthroughs from real provider payloads. Emails, IDs, and secrets anonymised; shapes mirror common events.
Find the customer email in a Stripe checkout event
ExampleYour handler needs the buyer's email but the Stripe docs are vague on exact nesting. The tree shows it lives under customer_details — copy the path.
Stripe checkout.session.completed body: $ ............................ Object(8) $.type ...................... "checkout.session.completed" $.data ...................... Object(1) $.data.object ............... Object(28) $.data.object.customer_details ... Object(4) $.data.object.customer_details.email ... "buyer@x.com" Copy → $.data.object.customer_details.email
Read a GitHub pull_request branch and action
ExampleYou're routing on the PR action and need the source branch. The tree shows both at a glance.
GitHub pull_request payload: $.action ............... "opened" $.pull_request ......... Object(36) $.pull_request.head .... Object(4) $.pull_request.head.ref "feature/login" $.pull_request.base.ref "main" Copy $.pull_request.head.ref for the source branch and branch your handler on $.action.
Confirm Stripe amount is in cents
ExampleBefore computing a refund you verify the amount field's type and unit. The number node and value make it obvious.
$.data.object.amount_total ... 4200 (number, cyan) $.data.object.currency ....... "usd" amount_total is the integer 4200 = $42.00 (Stripe uses the smallest currency unit). Divide by 100 in your handler; don't treat 4200 as dollars.
Iterate Shopify line items
ExampleYou need each SKU from an order webhook. The array's child count tells you how many items to loop, and the path gives the accessor.
$.line_items ........... Array(3) $.line_items[0].sku .... "TSHIRT-BLK-M" $.line_items[1].sku .... "MUG-001" $.line_items[2].sku .... "STICKER-PK" Copy $.line_items[0].sku as the template; loop indices 0..2 (the array shows Array(3)).
Distinguish a null optional field from a missing one
ExampleYour handler NPEs on an optional field. The tree shows whether the provider sent null or omitted the key entirely.
$.data.object.customer ... null ← sent as null vs a payload where no `customer` node appears (omitted). Guard for both: `obj.customer == null` handles the null case; key existence handles the omitted case.
Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
You saved the request headers along with the body
Invalid JSONWebhook tooling often shows headers (signature, content-type) above the body. If those lines are in the file, it isn't valid JSON and JSON.parse throws. Save the request body only — the signature header is metadata, not part of the JSON payload.
Payload is form-encoded, not JSON (some Shopify/Slack legacy)
Wrong formatA few webhook configurations deliver application/x-www-form-urlencoded (payload=%7B...%7D) rather than raw JSON. That's not JSON and won't parse. URL-decode and extract the JSON value first, then view it.
A batch of events, one JSON per line
Single document onlySome providers (and log replays) deliver newline-delimited events. The viewer parses the whole file as ONE JSON document, so an NDJSON batch fails. Wrap the events in a top-level array ([ {...}, {...} ]) before viewing, or inspect one event at a time.
Stripe amount looks 100x too big
By designStripe amounts are in the smallest currency unit — cents for USD. amount_total: 4200 is $42.00, not $4,200. The tree shows the raw integer; the unit interpretation is yours. Zero-decimal currencies (JPY) have no sub-unit, so the integer is the whole amount.
Signature can't be verified after viewing
ExpectedWebhook signatures are computed over the exact raw request bytes. Viewing or re-serialising the payload changes whitespace and key order, breaking the signature. Always verify the signature against the original raw body in your handler — never against a re-saved copy. The viewer is for understanding shape, not for verification.
Trailing comma or comment from hand-editing
Parse errorIf you trimmed a sample payload by hand and left a trailing comma or a // comment, strict JSON.parse fails. Clean it with json-format-fixer or remove the offending characters, then view.
Payload deeper than Max depth
PreservedIf a payload nests deeper than the Max depth setting, blocks at the cap show as Object(n) / Array(n) with child counts but can't be expanded. Raise Max depth (up to 20) and re-run. Nothing is dropped — only child rendering is capped.
Duplicate keys in a malformed payload
Last winsIf a payload contains the same key twice in one object, JSON.parse keeps the last occurrence and the tree shows one node. Well-formed provider payloads don't do this; if you see unexpected values, inspect the raw text.
Secrets or tokens in the payload
Stays localAPI keys, customer emails, and access tokens in a captured payload never leave your browser — parsing is client-side. The viewer doesn't redact them; if you need to share a sanitised sample, run it through json-anonymizer first.
File over the 2 MB free limit
Upgrade requiredFree uploads cap at 2 MB. Single webhooks are tiny, but a captured batch or one with large embedded data can exceed it. Pro raises the limit to 100 MB; or split the batch and inspect one event.
Frequently asked questions
Where is the customer email in a Stripe webhook?
For checkout events it's typically at $.data.object.customer_details.email. Drop a captured checkout.session.completed payload here, search email, and copy the exact path — provider field locations can vary by event, so confirm against your sample.
How do I know which event type a payload is?
Look at the discriminator: Stripe uses $.type, GitHub uses $.action plus the X-GitHub-Event header, Shopify uses the X-Shopify-Topic header, Slack uses $.type/$.event.type. The tree shows the body field; header-based discriminators come from the request headers, not the JSON.
Why does the Stripe amount look 100x too large?
Stripe amounts are in the smallest currency unit — 4200 is $42.00 in cents. The tree shows the raw integer; divide by 100 in your handler (except for zero-decimal currencies like JPY).
Can I verify the webhook signature here?
No. Signatures are computed over the exact raw request bytes; viewing or re-saving the payload changes them. Verify the signature against the original raw body in your handler. Use this tool to understand the payload shape only.
My payload won't parse. What's the usual cause?
Saved request headers in the file, a form-encoded body (payload=...), an NDJSON batch of multiple events, or a hand-edit trailing comma. Save the raw JSON body only, URL-decode if form-encoded, wrap batches in an array, and clean trailing commas (or use json-format-fixer).
Can I inspect several events at once?
The viewer parses one JSON document at a time. Wrap multiple event bodies in a top-level array to view them together, staying under 2 MB on free / 100 MB on Pro.
Does it match keys or values when I search?
Both, case-insensitively. Searching email finds the key; searching a buyer's email or an amount finds the value. Matching branches auto-expand so you see the location in context.
Is the payload uploaded anywhere?
No. It's parsed in your browser with native JSON.parse. Customer data and tokens in the payload never leave your machine. To share a sanitised sample, run it through json-anonymizer.
How do I turn a path into a reusable extractor?
Copy the path here, then paste it into json-path-extractor to pull that field out of every captured event, or use json-flattener to flatten a payload into dotted-key rows.
Can it generate a type for the payload?
Not directly. After you understand the shape, feed a sample to json-to-typescript to generate an interface for your handler, or json-to-zod for a runtime validator.
Does Max depth matter for webhooks?
Rarely — payloads are usually shallow, so the default depth of 20 renders them fully. Lower it only to inspect just the envelope of an unusually deep custom payload.
Is it free?
Yes — it runs in your browser and works on the free tier up to 2 MB, more than enough for any webhook. Pro removes the size limit (100 MB) and adds bulk and saved history.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.