Free Webhook Tester & Signature Verifier — Debug Stripe, GitHub, Slack, Shopify Webhooks
Technical Mastery Overview
What is a Webhook Tester?
A webhook tester is a tool that lets you inspect, validate, and debug webhook payloads and signatures without deploying code or exposing a live endpoint to the internet. There are two distinct problems it solves:
1. Testing that your endpoint receives webhooks correctly — tools like ngrok or smee.io forward live provider webhooks to your local machine so you can inspect exactly what a Stripe payment_intent.succeeded or a GitHub push event looks like in production. This is useful for building the integration.
2. Verifying that a specific signature is correct — when verification is failing in production (returning 401, 403, or your own "invalid signature" error), you need to reproduce the exact HMAC computation the provider used. This is what our tool does: given the raw payload, the secret, and the received signature header, it tells you whether they match and shows you the canonical string that was signed.
Most webhook debugging failures happen at stage 2 — the endpoint receives the payload fine, but the signature check fails due to body parsing order, encoding mismatches, or an incorrect canonical payload format. This tool isolates that problem without needing a running server.
How to Use the Verifier
1. Choose your provider
Select one of the built-in providers — Stripe, GitHub, Shopify, Slack — or choose Custom for any other service. Selecting a provider pre-configures the algorithm, encoding, and canonical payload format so you don't have to set them manually.
2. Paste the raw webhook body
Paste the exact raw request body your endpoint received into the body field. Do not reformat or parse it — the HMAC is computed over the original bytes, and any whitespace change will break the signature match.
3. Paste the signature header value
Paste the full signature header value (e.g. t=1700000000,v1=abc123 for Stripe, or sha256=abc123 for GitHub) into the header field. The tool parses it automatically and extracts the timestamp and signature values.
4. Enter your signing secret
Paste your primary signing secret in the first secret field. If you are mid-rotation and still accepting events signed with the old secret, paste the old secret in the fallback secret field — the tool tries both and tells you which one matched.
5. Click Verify
The result panel shows:
verified: true/false— whether the HMAC matchedalgorithmandencodingusedmatchedSecret— whether the primary or fallback secret matchedcomputedSignature— the HMAC your tool computed- Any issues: missing fields, timestamp outside window, signature mismatch
Additional controls
Timestamp override — enter a Unix timestamp manually to test against a specific point in time. Useful for testing stale-event rejection logic without having to replay a real event.
Tolerance seconds — configurable replay window (default 300s / 5 minutes). Applies to Stripe and Slack which embed a timestamp in their signatures. Reduce this for tighter replay protection.
Redact secrets/signatures — masks sensitive values in the UI panels so you can safely share screenshots or screen recordings without exposing secrets.
Live panels (update as you type, no button click needed):
- Canonical Payload — shows the exact string being signed (e.g.
1700000000.{"id":"evt_123"...}for Stripe,v0:1700000000:{"text":"hello"}for Slack) - Signature Generator — shows the computed HMAC in the correct provider format before verification, useful for constructing test requests
- cURL Sample — a ready-to-copy
curlcommand built from your current inputs, useful for replaying the event against a local endpoint
Custom provider mode
Select Custom to configure any provider not in the preset list:
- Algorithm — SHA-1, SHA-256, or SHA-512
- Encoding — hex or base64
- Prefix — the string prepended to the signature in the header (e.g.
sha256=,hmac-)
This covers providers like PayPal, Twilio, Braintree, and any internal webhook system.
Why Webhook Signature Verification Is Non-Negotiable
Webhook endpoints are designed to be called by external services — they're publicly accessible URLs that receive POST requests. Without verification, any actor with your endpoint URL can:
- Trigger fake order confirmations
- Send false payment success events
- Inject fraudulent user registrations
- Cause unauthorized workflow executions (CI/CD triggers, Slack commands)
- Flood your system with noise to mask real events
Signature verification confirms two things: (1) the event came from the legitimate provider who knows the shared secret, and (2) the payload wasn't modified in transit.
How HMAC Signing Works
Most major webhook providers use HMAC (Hash-based Message Authentication Code) with SHA-256, though SHA-1 and SHA-512 also appear across the ecosystem:
- Provider signs:
HMAC(secret, canonical_payload)→ signature - Provider sends: signature in a header alongside the raw payload
- You verify: recompute
HMAC(secret, raw_body)and compare
const crypto = require('crypto');
function verifyWebhook(rawBody, secretKey, receivedSignature) {
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(rawBody, 'utf8')
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(receivedSignature)
);
}
Never use === for signature comparison — it short-circuits on the first mismatch, allowing timing attacks to guess the signature byte by byte. Always use crypto.timingSafeEqual() or its equivalent.
Provider-Specific Signature Formats
Different providers structure their signatures differently:
Stripe
Stripe-Signature: t=1709296000,v1=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Stripe's canonical payload: {timestamp}.{raw_body}
const signedPayload = `${timestamp}.${rawBody}`;
const signature = hmacSHA256(webhookSecret, signedPayload);
The t= timestamp is included in the signed payload — this enables replay protection (reject if timestamp is more than 5 minutes old). A single header can contain multiple v1= values during secret rotation.
GitHub
X-Hub-Signature-256: sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
GitHub's canonical payload: just the raw request body, no timestamp.
const signature = 'sha256=' + hmacSHA256(webhookSecret, rawBody);
GitHub also sends a legacy X-Hub-Signature header using SHA-1 (not SHA-256). If you use the older endpoint or need to verify against the legacy header, select SHA-1 as the algorithm in the tool. Always prefer X-Hub-Signature-256 for new integrations.
Slack
X-Slack-Signature: v0=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
X-Slack-Request-Timestamp: 1709296000
Slack's canonical payload: v0:{timestamp}:{raw_body}
const baseString = `v0:${timestamp}:${rawBody}`;
const signature = 'v0=' + hmacSHA256(signingSecret, baseString);
The timestamp arrives in a separate header (X-Slack-Request-Timestamp) rather than in the signature header itself. In the tool, paste the timestamp into the Timestamp override field if your signature header doesn't include it directly.
Shopify
X-Shopify-Hmac-SHA256: base64-encoded-signature
Shopify uses Base64-encoded (not hex) HMAC-SHA256 output, and signs the raw body without a timestamp prefix.
Use our Base64 Decoder to convert Shopify's Base64 signature to hex for comparison, or our Hash Generator to verify HMAC values manually.
The Raw Body Problem — The Most Common Bug
The single most common webhook verification failure: the body was already parsed (JSON.parse'd) before verification, so the bytes used for HMAC computation don't match the original raw bytes.
// Express.js — WRONG order
app.use(express.json()); // Parses body BEFORE route
app.post('/webhook', (req, res) => {
// req.body is now a JS object, not raw bytes — verification FAILS
verify(req.body, req.headers['x-signature']);
});
// Correct — read raw body for webhook routes
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// req.body is Buffer — raw bytes — verification SUCCEEDS
verify(req.body, req.headers['x-signature']);
});
Whitespace normalization, JSON key reordering, and character encoding changes all produce different bytes from the same JSON object. The provider signs the exact bytes they sent — you must verify against those same bytes.
The Canonical Payload panel in the tool shows you the exact string that gets signed based on your provider. If you paste your raw body there and it doesn't match what you computed server-side, the body was mutated before verification.
Replay Attacks and Timestamp Windows
A valid signature doesn't guarantee the event is fresh. An attacker can capture a legitimate webhook and replay it hours later. The solution: include a timestamp in the signature and reject stale events.
function isWithinWindow(timestamp, toleranceSeconds = 300) {
const now = Math.floor(Date.now() / 1000);
return Math.abs(now - timestamp) <= toleranceSeconds;
}
// Reject if older than 5 minutes
if (!isWithinWindow(webhookTimestamp)) {
return res.status(400).json({ error: 'Webhook timestamp too old' });
}
Five minutes is the standard tolerance window (Stripe uses this). Shorter windows are more secure but require tight clock synchronization between sender and receiver. The verifier's Tolerance seconds field lets you test any window — set it to 0 to test strict rejection, or increase it to test events with an old timestamp. Use our Timestamp Converter to verify webhook timestamps when debugging stale-event rejections.
Secret Rotation Without Dropping Events
When rotating a webhook secret, there's a window where the provider is using the new secret but you may still receive events signed with the old one. The verifier's Fallback secret field handles this directly — enter the new secret as primary and the old one as fallback. The tool tries both and reports which matched.
function verifyWithFallback(rawBody, signature, secrets) {
for (const secret of secrets) {
const computed = hmacSHA256(secret, rawBody);
if (timingSafeEqual(computed, signature)) {
return true;
}
}
return false;
}
// During rotation: try new secret first, fall back to old
const isValid = verifyWithFallback(rawBody, signature, [newSecret, oldSecret]);
The rotation window should be short — a few minutes to an hour — then remove the old secret from the fallback list.
Debugging Failed Verification
When signature verification fails, check in this order:
- Raw body integrity — are you using the raw request bytes, not a parsed object?
- Encoding — is the signature hex or Base64? Are you comparing hex-to-hex?
- Canonical payload format — does it include a timestamp prefix? What's the exact format?
- Secret — is it the webhook secret, not the API key? Are there leading/trailing spaces?
- Timestamp — is the event within the tolerance window?
- Header name — is the signature in
X-Hub-Signature-256orX-Hub-Signature? (SHA-256 vs SHA-1)
The Canonical Payload panel shows the exact string signed. The Signature Generator panel shows the computed HMAC before you run verification — compare these manually against what your server computed to isolate the mismatch.
Building the Test Workflow
- Construct the test request using the tool's cURL Sample output, or build it with our cURL Generator
- Compute the expected signature locally with the Signature Generator panel
- Run verification and check which secret matched and whether the timestamp is within window
- Validate the JSON payload structure with our JSON Formatter and JSON Schema Validator
- Sanitize logs with our PII Redactor before sharing in incident reports
For generating strong webhook secrets, use our Password Generator with 32+ characters — the HMAC security is bounded by the secret's entropy.
Experience it now.
Use the professional-grade Webhook Signature Verifier with zero latency and 100% privacy in your browser.