A webhook secret, also called a signing secret, is a random key shared between a webhook provider and your endpoint. The provider uses it to compute an HMAC signature for every delivery, and your endpoint uses the same key to recompute that signature. If they match, the request came from someone who knows the secret and the body wasn't changed.
# .env
STRIPE_WEBHOOK_SECRET=whsec_5a1b9c... # issued by Stripe, one per endpoint
GITHUB_WEBHOOK_SECRET=9f2c4e7a1b... # chosen by you when adding the webhook
How a webhook secret is used
- When you create the webhook, the provider generates a secret (Stripe, Standard Webhooks providers) or you supply one (GitHub, many self-hosted tools).
- For each delivery, the provider signs the body with it:
HMAC-SHA256(secret, signed_string). - Your endpoint loads the same secret from its configuration, recomputes the signature over the raw body, and compares.
The secret itself is never sent with the request. Only the signature is.
Secret formats
| Provider | Format | How it's used as the HMAC key |
|---|---|---|
| Stripe | whsec_... | The whole string, prefix included |
| GitHub | Any string you choose | As-is |
| Shopify | The app's client secret | As-is |
| Standard Webhooks | whsec_ + base64 | Strip whsec_, then base64-decode the rest |
That last row catches people out. The Standard Webhooks key is the decoded bytes, not the text after the prefix.
Generating your own secret
When the provider asks you to choose one, generate at least 32 random bytes:
openssl rand -hex 32
# or
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Don't use a password, a word or anything you've used elsewhere.
Storing a webhook secret
- Keep it out of code. Load it from an environment variable or a secret manager such as AWS Secrets Manager, GCP Secret Manager, Vault or Doppler.
- One secret per endpoint and environment. Separate secrets for production, staging and local development mean a leaked dev secret can't be used to forge production events.
- Never log it. Don't print it in errors, and be careful with request-logging middleware that dumps configuration.
- Restrict access. Only the service that verifies webhooks needs it.
Rotating a webhook secret
Rotate on a schedule and immediately if a secret leaks. To avoid rejecting legitimate deliveries mid-rotation:
- Generate the new secret at the provider. Good providers then sign with both secrets for a while. The Webhook Relay Outbound Gateway, for example, signs every delivery with both the new and previous secret for 24 hours, so the
webhook-signatureheader carries two signatures. - Deploy the new secret to your endpoint, or accept either secret during the overlap.
- Confirm deliveries verify with the new secret, then remove the old one.
If the provider supports only one secret at a time, make your verifier accept a list of secrets, add the new one, rotate at the provider, then remove the old one.
Common mistakes
- Using the wrong environment's secret. Stripe test-mode and live-mode secrets differ, and every Stripe endpoint has its own. A mismatch makes every signature fail.
- Trailing whitespace or quotes. A newline copied into an environment variable silently changes the key.
- Using the secret text instead of the decoded key for Standard Webhooks.
- Sharing one secret across many customers when you send webhooks. Each customer endpoint should have its own, so one leak doesn't compromise everyone.
- Treating a secret in the URL as enough. A token in the query string ends up in access logs and doesn't protect the body from tampering. Sign the body.
To check a secret against a captured request, use the signature verifier or the HMAC calculator. Both run in the browser.
