invalid_signature
hooksentinel error invalid_signature — the request's signature header didn't match the signature computed from the body and secret. Common causes and the fix.
Summary
| Field | Value |
|---|---|
| Code | invalid_signature |
| HTTP status | 401 |
| Retryable | No |
What caused it
hooksentinel computed a signature from the raw request body (and, for HMAC providers, your configured secret) and it did not match the signature the provider sent in the request header. The header was present and well-formed — missing_signature_header and malformed_signature_header are separate codes for those cases — the computed value just doesn't agree with it.
The most common causes, roughly in order of frequency:
- Wrong secret. A test-mode secret configured against a live-mode endpoint (or vice versa), a stale secret left over after rotating it in the provider dashboard, or a secret for the wrong webhook endpoint if you have more than one registered.
- The body was mutated before verification. Something between the provider and hooksentinel re-serialized the JSON (different key order or whitespace), decoded and re-encoded it, or a proxy/CDN modified it in transit. This produces a different byte sequence than what the provider actually signed, even though the content looks identical.
- A genuinely spoofed request. Someone sent a request to your webhook URL without a valid secret. This is exactly the case signature verification exists to catch — the
401is correct behavior, not a bug.
The fix
Check the secret first. Confirm the value in your environment matches what the provider dashboard shows for the specific endpoint receiving traffic, and that you haven't mixed up test/live secrets:
import { stripe } from '@hooksentinel/core';
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!, // must match this exact endpoint's signing secret
});If the secret is correct, check for body mutation. Confirm nothing upstream of hooksentinel touches the body — most often this shows up as missing_raw_body instead, but a partial mutation (e.g. a proxy that only rewrites certain headers/fields) can produce invalid_signature instead. Log the raw body length and a hash of it at the edge of your infrastructure and compare against what your origin server receives, to rule out a proxy in between.
If you're testing locally, make sure you're using the CLI-issued secret (e.g. stripe listen prints its own whsec_... value, different from your dashboard's production secret) and that it's the one your local .env points to.
If none of the above resolves it and you're confident the request is legitimate, use createTestSigner (see Testing) to produce a known-good signed request locally and compare its shape against what your server is actually receiving, to isolate whether the issue is in your config or in transit.
Last updated on