"Why is my signature verification failing?" is the most common webhook debugging question. You capture the webhook, see the headers and body, compare your secret, re-read the provider docs, and still can't figure out why constructEvent throws.
The problem is that signature verification is invisible. You can't see the expected hash, the received hash, or whether the timestamp is stale. You're debugging blind.
Today we're shipping signature verification as a first-class feature in webhooks.cc. Two modes: paste a secret and verify any request instantly in the dashboard, or configure it on the endpoint and every incoming webhook is verified automatically.
Mode 1: Verify in the browser
Open any captured request, click the Signature tab, paste your signing secret, and click Verify.
The verification runs entirely in your browser. Your secret is never sent to our servers — it stays in the page until you close the tab.
The dashboard auto-detects the provider from the request headers. If it sees stripe-signature, it selects Stripe. If it sees x-hub-signature-256, it selects GitHub. You can override the detection if needed.
When verification fails, the dashboard shows you exactly what went wrong:
- Expected vs. received signature — the computed hash and the hash from the header, side by side, with copy buttons
- Timestamp analysis — for providers that include timestamps (Stripe, Slack, Paddle), whether the request falls within the 5-minute tolerance window
- Provider-specific debugging tips — Stripe tells you to check the endpoint-specific secret vs. the global one; GitHub reminds you about the
sha256=prefix
This is for one-off debugging. You captured a webhook, something's wrong, you want to check if the signature is valid right now.
Mode 2: Automatic verification on every request
Open your endpoint's settings and configure a signing provider and secret in the Signature Verification section. Select the provider from the dropdown, paste the secret, and save.
The secret is encrypted with AES-256-GCM before it touches the database. It's never visible again after you save it — the API returns hasSigningSecret: true, never the plaintext.
Once configured, the Rust receiver verifies every incoming request automatically. Verification runs as an async task after the response is sent — it adds zero latency to the webhook delivery. Results are stored on each request and surfaced everywhere:
- Dashboard request list — a shield icon on each request: green for valid, red for invalid, nothing when verification isn't configured
- Signature tab — shows the stored result with full details (no need to paste a secret)
- SDK —
request.signatureVerifiedistrue,false, ornull - CLI tunnel — forwarded requests include
X-Signature-VerifiedandX-Signature-Providerheaders - MCP — AI agents see verification status on every request
13 providers supported
| Provider | Algorithm | Notes |
|---|---|---|
| Stripe | HMAC-SHA256 | Timestamp in stripe-signature header |
| GitHub | HMAC-SHA256 | sha256= prefix on signature |
| Shopify | HMAC-SHA256 | Base64-encoded signature |
| Twilio | HMAC-SHA1 | Signs URL + sorted form params |
| Slack | HMAC-SHA256 | Timestamp prefix: v0:ts:body |
| Paddle | HMAC-SHA256 | Timestamp in paddle-signature header |
| Linear | HMAC-SHA256 | Standard hex signature |
| Vercel | HMAC-SHA1 | Hex-encoded |
| GitLab | Token comparison | x-gitlab-token header, not HMAC |
| Clerk | HMAC-SHA256 | Standard Webhooks format via svix headers |
| Discord | Ed25519 | Public key, not shared secret |
| Standard Webhooks | HMAC-SHA256 | Base64 secret, whsec_ prefix |
| Generic HMAC | HMAC-SHA256 | You specify the header name |
SendGrid uses IP allowlisting instead of signatures. It doesn't appear in the provider dropdown — instead, a note below the selector explains that SendGrid is not supported for signature verification.
Generic HMAC handles any provider not in the list — point it at the header that contains the signature and it verifies with HMAC-SHA256.
Three ways to configure
You can set up verification through the dashboard, the SDK, or the REST API.
Dashboard — open endpoint settings, select a provider, paste the secret. Good for quick setup and one-off debugging.
SDK / programmatic — configure signing from code, with secrets in your .env files where they belong:
import { WebhooksCC } from "@webhooks-cc/sdk";
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
await client.endpoints.update("my-endpoint", {
signingProvider: "stripe",
signingSecret: process.env.STRIPE_WEBHOOK_SECRET!,
});REST API — the same thing via curl, useful in CI or setup scripts:
curl -X PATCH https://webhooks.cc/api/endpoints/my-endpoint \
-H "Authorization: Bearer $WHK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"signingProvider": "stripe", "signingSecret": "'$STRIPE_WEBHOOK_SECRET'"}'All three produce the same result: the secret is encrypted with AES-256-GCM and stored on the endpoint. Every incoming request is verified automatically from that point on.
Asserting on verification in tests
import { matchAll, matchVerified, matchMethod } from "@webhooks-cc/sdk";
const request = await client.requests.waitFor("my-endpoint", {
timeout: "30s",
match: matchAll(matchMethod("POST"), matchVerified()),
});
// request.signatureVerified === true
// request.signingProvider === "stripe"matchVerified() filters for requests where the signature was valid. matchUnverified() filters for requests where verification failed. Requests with no signing config (signatureVerified === null) match neither.
To clear the signing config:
await client.endpoints.update("my-endpoint", {
signingProvider: null,
});The on-ramp: browser to endpoint
The three paths serve different stages of a project. During initial integration, you paste a secret in the browser to debug one request. Once the signature checks out, you save it to the endpoint — through the dashboard, the SDK, or the API — and verification becomes automatic.
A typical progression:
Capture a webhook
Point Stripe at your webhooks.cc endpoint. A webhook arrives.
Verify in the browser
Open the request, go to the Signature tab, paste your whsec_... secret, click Verify. The signature checks out.
Save to endpoint
Click "Save to Endpoint Settings" at the bottom of the Signature tab. The endpoint settings dialog opens with Stripe pre-selected. Paste the secret again (it wasn't stored from the browser step), save.
Automatic from here
Every future request is verified automatically. Green shields in the request list. signatureVerified: true in the SDK. X-Signature-Verified: true in the CLI tunnel.
What it looks like when things go wrong
The value of this feature is clearest when verification fails. Instead of a generic "signature mismatch" error from your Stripe SDK, the dashboard shows:
- The exact signature your server computed vs. what Stripe sent
- Whether the timestamp was stale (outside the 5-minute window)
- Whether the signature header was missing entirely (the request may not be from Stripe)
- A provider-specific tip pointing you to the most likely fix
This turns "why is my verification failing?" from a 30-minute debugging session into a 10-second glance.
Signature Verification Guide
Full setup guide for browser and endpoint verification modes.