How to Generate Fake Webhook Payloads for Stripe, GitHub, and Slack

July 7, 2026

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 POST

For 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.json

For 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.