Signature Verification

Verify incoming webhooks with HMAC-SHA256, HMAC-SHA1, or Standard Webhooks.

Signatures prove a webhook came from the expected sender and not an attacker. The provider hashes the request body with a shared secret; hookstream recomputes the hash and compares with constant-time equality. Mismatch means 401.

Hookstream ships preconfigured signature verification for 15+ providers — Stripe, GitHub, Shopify, Clerk, Svix, Linear, and more. Use a source template and the secret is the only field you need to set.

Supported algorithms

Algorithmverification_typeUsed by
HMAC-SHA256hmac-sha256Stripe, GitHub, Shopify, Slack, most modern providers
HMAC-SHA1hmac-sha1Legacy providers (pre-2020 GitHub, some CI systems)
Standard Webhooksstandard-webhooksSvix, Clerk, Resend, and other Standard Webhooks adopters

HMAC-SHA256 is the default recommendation. SHA-1 is still supported for legacy integrations but should not be chosen for new work.

How each algorithm works

HMAC-SHA256

The sender computes HMAC-SHA256(secret, request_body) and puts the hex digest in a header (for example Stripe-Signature or X-Hub-Signature-256). Hookstream:

  1. Reads the signature header you configured on the source.
  2. Recomputes the HMAC over the raw request body.
  3. Compares using constant-time equality.

A common prefix like sha256= is stripped automatically before comparison.

HMAC-SHA1

Same flow, but the hash function is SHA-1. Configure with verification_type: "hmac-sha1".

Standard Webhooks

Standard Webhooks is an open spec built on HMAC-SHA256 with a fixed set of headers:

  • webhook-id — unique event id
  • webhook-timestamp — Unix timestamp
  • webhook-signaturev1,<base64_signature> (multiple versions allowed, comma separated)

The signed string is webhook-id.webhook-timestamp.body and the secret is base64-encoded. Configure with verification_type: "standard-webhooks".

Configure verification

1

Get the signing secret from your provider

In the provider's dashboard, create or view the webhook endpoint and copy the signing secret. For Stripe this is whsec_...; for GitHub it's whatever you typed into the "Secret" field when creating the hook.

2

Create a source with the secret

Either use a template from GET https://hookstream.io/v1/sources/templates or post the fields directly:

bash
curl -X POST https://hookstream.io/v1/sources \ -H "X-API-Key: $HOOKSTREAM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Stripe Webhooks", "verification_type": "hmac-sha256", "verification_secret": "whsec_...", "verification_header": "stripe-signature" }'
3

Point your provider at the ingest URL

Copy the URL from the source detail page — it looks like https://hookstream.io/v1/ingest/src_<id> — and paste it into the provider's webhook settings.

4

Verify with a real event

Send a test event from the provider (or use the Signature Validator to simulate one). Successful requests land in your Events page with verification_status: "verified".

Signature verification runs on the raw request body. If something upstream (a proxy, a transform) modifies the body before hookstream sees it, every signature will fail. Hookstream handles this correctly because it reads the body exactly once.

Test and debug

Two free tools help when a signature keeps failing:

  • Signature Validator — paste the secret, headers, and body to see step-by-step verification output.
  • HMAC Calculator — compute an HMAC over arbitrary input to cross-check what your sender should be producing.

Events that fail verification are still stored (with verification_status: "failed") so you can inspect the headers and body in the dashboard and figure out what went wrong.

Next Steps

Sources API

Full source schema, including every verification field.

Learn More
IP Filtering

Add a network-layer allowlist on top of signature verification.

Learn More
Ask a question... ⌘I