How to Test Webhooks: Tools, Techniques, and Best Practices
Testing webhooks is annoying because you can't just call a function — you need a running server with a public URL and a service willing to send you events. Here's every approach worth knowing.
Jason Warner
March 22, 2026
How to Test Webhooks: Tools, Techniques, and Best Practices
Webhooks are uniquely annoying to test. Unlike a REST endpoint you can call with curl, a webhook handler needs to receive an inbound HTTP request — which means you need a running server, a public URL, and a service that will actually send events to it.
Here's a complete guide to every testing technique worth knowing.
Layer 1: Unit Testing Your Handler Logic
Extract your processing logic into a pure function and test it directly:
// handler.js — pure function, no HTTP
async function processStripePayment(event) {
if (event.type !== 'payment_intent.succeeded') return { handled: false };
const intent = event.data.object;
await activateSubscription(intent.metadata.userId, intent.amount_received);
return { handled: true, userId: intent.metadata.userId };
}
// handler.test.js
test('activates subscription on payment_intent.succeeded', async () => {
const mockEvent = {
type: 'payment_intent.succeeded',
data: { object: { metadata: { userId: 'u_123' }, amount_received: 4900 } }
};
const result = await processStripePayment(mockEvent);
expect(result.handled).toBe(true);
expect(activateSubscription).toHaveBeenCalledWith('u_123', 4900);
});
Cover edge cases here: unknown event types, missing fields, idempotency logic.
Layer 2: Inspecting Real Payloads
Webhook.site — Instant URL, no account needed. Good for a quick look at payload shape.
Bluejay Relay Capture URL — Persistent, logged captures. More useful for ongoing development since history persists and you can replay captured events later.
curl — If you know the payload format, construct it manually:
curl -X POST http://localhost:3000/webhook \
-H 'Content-Type: application/json' \
-d '{ "type": "payment.succeeded", "data": { "amount": 4900 } }'
Layer 3: Testing with a Local Tunnel
ngrok: ngrok http 3000 — most common, dashboard lets you replay recent requests.
Stripe CLI: stripe listen --forward-to localhost:3000/webhook — Stripe-specific, handles signatures automatically.
Bluejay Relay + ngrok: Point the source at your Bluejay Relay integration URL. Run ngrok (ngrok http 3000) and add your ngrok public URL as a destination in Bluejay Relay. Bluejay Relay can't reach localhost directly — ngrok bridges that gap. Events are logged in Bluejay Relay even when your local server is down; replay them when you're back up.
Layer 4: Integration Tests
test('POST /webhook processes payment.succeeded', async () => {
const payload = JSON.stringify({
type: 'payment_intent.succeeded',
data: { object: { id: 'pi_test', amount_received: 4900 } }
});
const sig = generateSignature(payload, process.env.WEBHOOK_SECRET);
const res = await request(app)
.post('/webhook')
.set('Content-Type', 'application/json')
.set('X-Stripe-Signature', sig)
.send(payload);
expect(res.status).toBe(200);
await new Promise(r => setTimeout(r, 100));
const sub = await db.subscriptions.findOne({ stripeId: 'pi_test' });
expect(sub.status).toBe('active');
});
Key cases to cover: valid signature → 200, invalid signature → 401, unknown event type → 200 (don't error), duplicate event ID → idempotent.
Layer 5: Replay Testing
With Bluejay Relay, make a code change then replay historical events through it:
- Navigate to your Integration's logs
- Select events to replay
- Click Bulk Replay (or Time-Travel Replay to modify payload, or Dry Run to preview)
Common Pitfalls
- Forgetting to test signature verification with an invalid signature
- Testing with toy payloads instead of real captured ones
- Not testing idempotency (send same event twice, verify result is the same)
- Not testing the async processing failure path
Checklist
- Handler logic unit tested with real payload shapes
- Valid and invalid signature cases covered
- Idempotency test for duplicate event IDs
- Unknown event type handled gracefully (200, no error)
- Local tunnel set up for live testing
- Integration test covers full request → response → side effect chain
- Replay workflow available for regression testing
Bluejay Relay makes webhook testing easier: capture real payloads, inspect every delivery end-to-end, and replay after code changes on the paid plans. Free to start.
Build more reliable webhook workflows.
Capture, transform, and retry webhooks with full observability. Free to start, no credit card.