Mock Webhook Payloads: Test Your Handler Without a Real Provider
A webhook, from the receiving server's side, is nothing more than a JSON
body someone else's server POSTs to your endpoint. Stripe doesn't send
"a payment," it sends {"type": "payment_intent.succeeded", "data": {...}}
to a URL you registered. Once you see it that way, mock webhook
payloads stop being a special problem — it's the same "write a JSON
template shaped like the provider's documented event, fill it with
generated values" technique already used to mock a REST
response or mock a GraphQL
query, pointed at an event envelope
instead of an API response. This post covers templating that envelope,
keeping IDs consistent across a batch of deliveries, and replaying the
result against your own endpoint to test webhooks locally — plus where
this approach stops, because it doesn't produce a signed request.
Templating mock webhook payloads: the event envelope
Most webhook providers wrap the thing that actually changed in a thin,
consistent envelope: a discriminator field naming the event type, an id
for the delivery itself, a timestamp, and a nested object holding the
resource. A Stripe-style payment_intent.succeeded event looks like this
as a JsonFabrica template:
{
"id": "evt_<createSeq('evtId', 'uuid')><getSeq('evtId')>",
"type": "payment_intent.succeeded",
"created": "<getRandomDate('2026-01-01', '2026-12-31')>",
"data": {
"object": {
"id": "pi_<createSeq('piId', 'uuid')><getSeq('piId')>",
"amount": "<getRandomNumber(500, 250000)>",
"currency": "usd",
"status": "succeeded"
}
}
}
That's one or two levels of nesting — data.object — not the deep tree a
GraphQL response can turn into, which is what makes webhook envelopes
comparatively quick to template. A GitHub-style event follows the same
pattern with different field names: a top-level action, a repository
object, and a nested resource like pull_request or issue:
{
"action": "opened",
"number": "<getRandomNumber(1, 5000)>",
"pull_request": {
"id": "<getRandomNumber(1000000, 9999999)>",
"title": "<getRandomTextWithSpaces(20, 40)>",
"state": "open"
},
"repository": {
"full_name": "acme/<createSeq('repoSlug', 'uuid')><getSeq('repoSlug')>"
}
}
Whatever provider you're integrating with, the process is the same: open the provider's webhook docs, copy the field names and nesting of the event type you care about, and swap literal example values for generator functions. Generating from either template through the generate API returns a JSON body you can drop straight into a test.
Stable, incrementing IDs across a batch of events
A single generated event is enough to check your handler parses the
shape correctly. A test suite usually needs several distinct events —
one per test case, or a sequence simulating a resource's lifecycle — and
random IDs on each one make it hard to tell, from a log or an assertion,
which generated event was which. createSeq and getSeq fix that the
same way they do for any other unique field: declare a named counter
once, then draw the next value into each event.
{
"id": "evt_<createSeq('webhookEvt', 'number', 1000, 1)><getSeq('webhookEvt')>",
"type": "charge.refunded",
"created": "<getRandomDate('2026-01-01', '2026-12-31')>",
"data": {
"object": {
"id": "ch_<getSeq('webhookEvt')>",
"amount_refunded": "<getRandomNumber(100, 50000)>"
}
}
}
Generated as a batch with count: 20 against this template, each
delivery gets its own strictly increasing webhookEvt value baked into
both id and data.object.id, so a test asserting "the fifth event
processed had id evt_1004" doesn't have to guess. See Unique Test Data
Generation for more on how
createSeq/getSeq behave across runs and namespaces.
Linking a batch of events to the same parent resource
Some webhook test scenarios need several events that all point back at
one resource — three line_item.added deliveries for the same order, for
instance, the way a checkout flow might fire one event per item added to
a cart. That's a relational batch: generate the parent order alongside
the line_item_added events and add a relations entry mapping the
child's orderId field to the parent.
{
"seed": 42424242,
"documents": [
{
"templateId": "tpl_order",
"alias": "order",
"count": 3
},
{
"templateId": "tpl_line_item_added",
"alias": "event",
"count": 9,
"relations": {
"data.object.orderId": { "from": "order.id", "strategy": "round-robin" }
}
}
]
}
Posted to POST /v1/batches, this
produces 3 orders and 9 events, each event's data.object.orderId filled
from one of the 3 orders in round-robin order — round-robin is the only
relation strategy the batch API supports; any other value is rejected.
Small batches like this one return synchronously (200, with results
inline); larger batches are queued and return 202 with a batchId you
poll via GET /v1/batches/{batchId}.
Replaying the generated payload against your endpoint
Generating the JSON is only half the point — a webhook test needs that body actually delivered to the handler under test. For a one-off check, curl against your local dev server does it:
curl -X POST http://localhost:3000/webhooks/stripe \
-H "Content-Type: application/json" \
-d @generated-event.json
Wired into a test's setup step, the same thing happens over fetch
instead of a shell-out:
async function deliverWebhook(payload) {
const res = await fetch("http://localhost:3000/webhooks/stripe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
return res.status;
}
// generatedEvents is the array of documents from a batch response
for (const event of generatedEvents) {
await deliverWebhook(event);
}
This is replay, not interception — JsonFabrica isn't a sandbox or proxy
sitting between the real provider and your server, and it isn't
listening for or forwarding live traffic. It generates a JSON body; your
test script is the thing that POSTs it to your own endpoint. If you
specifically need to relay a provider's real, live webhook traffic to a
local server for manual debugging, that's what the Stripe CLI's
listen --forward-to or a tunnel like smee.io is for — a different tool
solving a different problem than generating test payloads.
What this doesn't do: signature verification
Providers that sign webhook deliveries — Stripe's Stripe-Signature
header is the common example — expect the receiving handler to verify
that signature against the raw request body and a shared signing secret
before trusting the payload. JsonFabrica has no HMAC or signing function
in its generator catalog, so a generated body has no valid signature
attached to it. If your handler rejects unsigned or badly-signed
requests — and it should, in production — a generated payload alone won't
get past that check.
The fix isn't to skip signature verification in tests; it's to sign the
generated body with the provider's own tooling. Stripe's SDKs expose a
stripe.webhooks.generateTestHeaderString helper (and the Stripe CLI has
an equivalent) that takes a JSON payload and a signing secret and
produces a header your handler will accept. Generate the body with
JsonFabrica, sign it with the provider's helper, then send both together
— JsonFabrica's job stops at producing the realistic, shaped, and
internally-consistent JSON that goes into that signing step.
FAQ
How do I mock a webhook payload for testing? Write a JSON template that mirrors the provider's documented event shape, usually an envelope with a type or event field, an id, a created timestamp, and a nested object carrying the resource that changed. Generate from that template and POST the resulting JSON straight at your own endpoint under test with curl or a small script, instead of waiting for the real provider to send an event.
Can I generate a valid signed Stripe webhook test payload with
JsonFabrica?
JsonFabrica generates the JSON body only; it has no signing or HMAC
function, so it can't produce a valid Stripe-Signature header on its
own. If your handler verifies that signature, pair the generated body
with Stripe's own signing helper (stripe.webhooks.generateTestHeaderString
or the Stripe CLI) to produce a header that matches the body's content.
How do I generate webhook test data with consistent, incrementing
IDs?
Use createSeq to declare a named counter once and getSeq to draw the
next value into each event's id field, so a batch of generated
deliveries gets distinct, strictly increasing IDs instead of colliding
random ones. The same sequence can back both the event id and a resource
id inside data.object if your test needs both to increment together.
How do I generate multiple webhook events that reference the same
parent resource, like several line-item events for one order?
Generate the parent resource and the events in one batch request and add
a relations entry on the event document mapping its orderId field to
the parent order's id with round-robin as the strategy —
round-robin is the only relation strategy the API supports. Each
generated event then references one of the batch's order records instead
of a random, disconnected id.
Does JsonFabrica act as a webhook proxy or sandbox that intercepts real provider events? No. JsonFabrica only generates the JSON payload; it doesn't listen for or forward real webhook traffic from Stripe, GitHub, or any other provider. You generate the payload and POST it yourself, in a test setup step, at your own endpoint — tools like the Stripe CLI or smee.io are what you'd reach for if you specifically need to relay live provider events.
Templating a webhook envelope is no different from templating any other JSON shape — the win is generating a batch of realistic, internally consistent deliveries instead of hand-typing one event and reusing it for every test case. Try it against your own endpoint with the batch generation API, pairing the generated body with your provider's signing helper wherever the handler under test checks a signature.
Generate realistic test data with JsonFabrica
Describe the shape of your data once, then generate as many fresh, realistic JSON documents as you need via a simple API call.