How to extract fields from webhook payloads using jsonpath
- Step 1Capture one real payload — Use the platform's test sender (Stripe Dashboard, GitHub webhook redelivery) or capture a live one via an ngrok tunnel or webhook.site. Copy the full JSON body of a single event.
- Step 2Drop or paste the payload — Provide one event as a single JSON document. Stripe events have a
data.objectwrapper; GitHub events expose the resource at the top level. Don't paste several events together — that failsJSON.parse. - Step 3Build the path for each field you need — Stripe:
$.typefor the event type,$.data.object.customerfor the customer,$.data.object.amountfor the amount. GitHub:$.action,$.repository.full_name,$.commits[*].id. Verify each against the real payload. - Step 4Add a routing filter if needed — Use a single-operator filter to select by value, e.g.
$.data.object[?(@.amount >= 10000)]for high-value charges. Remember there is no&&— one condition per filter. - Step 5Run a presence check — Before assuming a field exists, confirm it: if
$.data.object.customerreturns0 matches, your handler should reject or branch. Existence filters ([?(@.customer)]) test presence across an array. - Step 6Port the verified paths into your handler — Reuse the confirmed paths with jsonpath-plus (Node) or jsonpath-ng (Python), or as plain object access. For reshaping the payload, use jq or the sibling JSON tools.
Where key fields live, per platform
Common webhook fields and the JSONPath that reaches them. Verify against your actual payload — platforms version their schemas.
| Platform | Field | JSONPath |
|---|---|---|
| Stripe | Event type | $.type (e.g. payment_intent.succeeded) |
| Stripe | Customer id | $.data.object.customer |
| Stripe | Charge amount (cents) | $.data.object.amount |
| GitHub | Action | $.action (e.g. opened, closed) |
| GitHub | Repository | $.repository.full_name |
| GitHub | Commit ids in a push | $.commits[*].id |
| Shopify | Order total | $.total_price |
| Twilio | Message status | $.MessageStatus |
| Any | Any id, depth unknown | $..id |
Routing and defensive checks with filters
Single-predicate filters cover most webhook routing. For multi-condition routing, apply one here and the rest in code.
| Intent | JSONPath | Behaviour |
|---|---|---|
| High-value charges | $.data.object[?(@.amount >= 10000)] | Matches the object only if amount is at least 10000 |
| Specific event type | $[?(@.type == 'invoice.paid')] | Selects events whose type equals the literal |
| Has a customer attached | $.data.object[?(@.customer)] | Existence check — true even if customer is null |
| Refunded charges | $.data.object[?(@.refunded == true)] | Boolean compare; literal true/false/null are parsed as their types |
| Two conditions at once | [?(@.a == 1 && @.b == 2)] | Unsupported — returns 0 matches; split the logic |
Cookbook
Real (anonymised) Stripe and GitHub payload fragments with the path that pulls each field. [matches: N] is the live count.
Read the Stripe event type and customer
ExampleStripe wraps the resource in data.object. The type sits at the top level; the customer one level inside. Confirm both before branching in your handler.
Path: $.type
Output [matches: 1]: "payment_intent.succeeded"
Path: $.data.object.customer
Input:
{ "type": "payment_intent.succeeded",
"data": { "object": { "customer": "cus_123", "amount": 4200 } } }
Output [matches: 1]: "cus_123"Flag a high-value charge for manual review
ExampleA single numeric filter on data.object returns the object only when the amount clears the threshold — drive a 'route to review queue' branch off the match count.
Path: $.data.object[?(@.amount >= 10000)]
Input:
{ "data": { "object": { "amount": 25000, "currency": "usd" } } }
Output [matches: 1]:
{ "amount": 25000, "currency": "usd" }List all commit ids from a GitHub push
ExampleGitHub push events carry a commits array. [*].id projects every commit hash so you can fan out per-commit processing.
Path: $.commits[*].id
Input:
{ "ref": "refs/heads/main", "commits": [
{ "id": "a1b2", "message": "fix" },
{ "id": "c3d4", "message": "docs" }
] }
Output [matches: 2]:
[ "a1b2", "c3d4" ]Find an id when you don't know the nesting
ExampleWhen a payload version moves an id around, recursive descent collects every id so you can spot the one you need without re-reading the schema.
Path: $..id
Input:
{ "id": "evt_1",
"data": { "object": { "id": "ch_9", "customer": "cus_1" } } }
Output [matches: 2]:
[ "evt_1", "ch_9" ]Defensive presence check before processing
ExampleIf the customer is missing, the path returns 0 matches — your handler should reject rather than crash. Run this against the real payload to confirm the field is reliably present.
Path: $.data.object.customer
Input (guest checkout, no customer):
{ "data": { "object": { "amount": 999 } } }
Output [matches: 0]:
[]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.
Several events pasted together
Parse errorThe tool parses the whole input as one JSON document. Multiple events concatenated throw Unexpected non-whitespace character after JSON. Test one event per run, or wrap several in a JSON array [ ... ].
Querying the resource without the `data.object` wrapper
0 matchesStripe nests the resource under data.object. $.amount returns nothing; the real path is $.data.object.amount. Paste the full event so the wrapper is present.
Routing on two conditions
0 matches$.data.object[?(@.amount >= 10000 && @.currency == 'usd')] is unsupported and matches nothing — there's no &&. Filter on amount here, then check the currency in code.
Presence check passes but value is null
Matched[?(@.customer)] matches even when customer: null (guest checkout can set it null rather than omitting it). Use [?(@.customer != null)] for a strict 'has a real customer' check.
Amount stored as a string
By designSome platforms send money as strings ("49.99"). Numeric filters still work via JS coercion ([?(@.amount > 10)] matches), but exact equality (== 49.99) won't match a string. Normalise types in your handler.
Negative index to get the latest commit
0 matches$.commits[-1] is unsupported. Use a reverse slice: $.commits[::-1][0] gives the last commit, or $.commits[-1:] returns it as a one-element array.
Payload field name typo
0 matches$.data.object.custmer (typo) returns an empty result rather than an error. Rebuild the path segment by segment to find where it stops resolving.
Payload larger than the free limit
BlockedFree tier caps input at 2 MB. Real webhooks are tiny, but a bulk export of stored events can exceed it — Pro raises the limit to 100 MB, or test a single event.
Expecting signature verification
Not supportedThis tool extracts fields; it does not verify Stripe/GitHub signatures. Always verify the signature header in your handler with the platform SDK before trusting any extracted value.
Frequently asked questions
Where is the Stripe customer id in the payload?
At $.data.object.customer. Stripe wraps the resource in data.object, so the event type is $.type but resource fields live one level deeper.
How do I route by event type?
Extract $.type (Stripe) or $.action (GitHub) to confirm the exact field name and value, then key a switch or handler registry off it in your code.
Can I match on amount AND currency together?
No — filters take a single predicate and && is unsupported (returns 0 matches). Filter on amount here, then check currency in your handler.
How do I get the latest commit from a push?
Negative index access isn't supported, so $.commits[-1] fails. Use a reverse slice: $.commits[::-1][0] for the last commit, or $.commits[-1:] for it as an array.
Does an existence filter confirm the field has a real value?
No — [?(@.customer)] only checks the key is present, so customer: null still matches. Use [?(@.customer != null)] to require a non-null value.
Is the payload sent anywhere?
No. Parsing and evaluation run entirely in your browser; payment amounts, customer ids, and PII never reach JAD Apps servers.
Can I paste several events to scan them at once?
Not as raw concatenated objects — that fails JSON.parse. Wrap them in a JSON array ([ {...}, {...} ]) and query with $[*]... or $[?(...)].
Why does `$.amount` return nothing for my Stripe event?
Because the field is nested: use $.data.object.amount. Paste the full event so the data.object wrapper is present, then build the path against the real structure.
Can it verify the webhook signature?
No — it only reads fields. Verify the signature header with the platform's SDK in your handler; never trust extracted values from an unverified payload.
How big a payload can it handle?
Up to 2 MB free and 100 MB on Pro. Individual webhooks are far smaller; the limit only matters for bulk event exports.
Can it reshape the payload, not just extract?
No. To rename keys use json-key-renamer, to drop keys use json-key-filter, and for arbitrary transforms use jq.
How do I turn extracted events into a table?
Extract the records into an array, then convert with json-to-csv; flatten nested fields first with json-flattener if needed.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.