How to Generate Fake Webhook Payloads for Stripe, GitHub, and Slack
Testing a webhook handler is awkward because you can't easily trigger the real event on demand — you'd have to actually complete a Stripe payment, push a commit, or post a Slack message every time you want to test your handler. The fix is generating realistic fake webhook payloads locally and posting them straight to your endpoint.
Step 1: Get the Real Payload Shape
Webhook payloads have very specific, deeply nested shapes defined by each provider — don't guess at the structure. Start from the provider's own documentation or the "event types" page, then use a tool like Dummy JSON Generator to fill in realistic values for the fields you actually check in your handler.
{
"id": "evt_1N3T6r2eZvKYlo2C",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_3N3T6r2eZvKYlo2C0X",
"amount": 4999,
"currency": "usd",
"status": "succeeded"
}
}
}Step 2: Use the Provider's Official CLI When One Exists
Stripe and GitHub both ship CLIs that generate and deliver realistic test webhook events for you — prefer these over hand-built payloads whenever available, since they stay in sync with the real schema automatically.
# Stripe CLI — forwards real test-mode events to your local server
stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger payment_intent.succeeded
# GitHub CLI — replay a real delivered webhook payload
gh api /repos/OWNER/REPO/hooks/HOOK_ID/deliveries
gh api /repos/OWNER/REPO/hooks/HOOK_ID/deliveries/DELIVERY_ID/attempts -X POSTFor Slack, there's no equivalent CLI — build a fake payload matching Slack's Events API shape and POST it directly instead.
Step 3: Sign the Payload Correctly
This is the step most fake-webhook tests get wrong. Every major provider signs webhook payloads with an HMAC signature, and your handler almost certainly verifies it before processing the event — so a fake payload with no signature, or a badly generated one, gets silently rejected before your test logic even runs.
// Reproducing Stripe's signature scheme for a local test
import crypto from 'crypto'
function signStripePayload(payload: string, secret: string, timestamp: number) {
const signedPayload = `${timestamp}.${payload}`
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex')
return `t=${timestamp},v1=${signature}`
}Use your webhook signing secret from the provider's test-mode dashboard, and attach the resulting header (Stripe-Signature, X-Hub-Signature-256 for GitHub, or Slack's X-Slack-Signature) to your test request exactly as the real provider would.
Step 4: POST It to Your Local Handler
curl -X POST http://localhost:3000/webhooks/stripe \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=1717000000,v1=abcdef..." \
-d @fake-payment-intent.jsonFor Slack and GitHub, expose your local server to the internet during manual testing with a tunnel tool (ngrok, Cloudflare Tunnel) if the provider needs to reach a public URL — but for automated tests, posting directly to your local handler as shown above is faster and doesn't depend on network access.
Step 5: Test Idempotency, Not Just the Happy Path
Real webhook providers retry delivery on timeout or error — meaning your handler will receive the same event more than once in production. Reuse the exact same fake payload (same event ID) and POST it twice in a test to confirm your handler doesn't double-process the event.
The Bottom Line
Use the provider's own CLI wherever one exists (Stripe, GitHub), fall back to hand-built payloads with correctly reproduced signatures for providers without one (Slack), and always test duplicate delivery — not just a single successful event — since that's the case that actually breaks in production.