Outbound Signing
Sign outbound webhook deliveries with HMAC for receiver verification.
Outbound signing mirrors inbound signature verification but from the other side: hookstream signs every outbound delivery so your receivers can verify it came from you, not from an attacker who guessed the URL.
How it works
You configure a secret on the destination
Stored in signing_config on the destination.
Hookstream computes HMAC(secret, body)
For every outbound request, using the configured algorithm (SHA-256 or SHA-1).
The signature is sent in a header
Default header is X-hookstream-Signature with a sha256= prefix.
Your receiver recomputes and compares
If the signatures match (constant-time compare), the request is verified.
Configure outbound signing
Set signing_config on an HTTP destination:
bashcurl -X PATCH https://hookstream.io/v1/destinations/dest_xyz \ -H "X-API-Key: $HOOKSTREAM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "signing_config": { "secret": "your_secret_here", "algorithm": "sha256", "header": "X-hookstream-Signature", "prefix": "sha256=", "include_timestamp": false, "timestamp_header": "X-hookstream-Timestamp" } }'
Fields
| Field | Default | Notes |
|---|---|---|
secret | — | Required. The shared secret. |
algorithm | sha256 | sha256 (recommended) or sha1. |
header | X-hookstream-Signature | Header the signature lands in. |
prefix | "{algorithm}=" | Prepended to the hex digest. Set "" to send raw hex. |
include_timestamp | false | When true, the signing payload becomes {unix_timestamp}.{body} (dot-separated) and the timestamp is also sent in timestamp_header. |
timestamp_header | X-hookstream-Timestamp | Only used when include_timestamp is true. |
The verification examples below assume include_timestamp: false. If you enable timestamps, the receiver must read X-hookstream-Timestamp, reconstruct the signing payload as `${timestamp}.${rawBody}`, and use that string instead of rawBody when computing the HMAC.
Use SHA-256. SHA-1 is only there for compatibility with older receivers — new integrations should never pick it.
Verify the signature on the receiver side
javascriptconst crypto = require("crypto");function verifyHookstream(req, rawBody) { const signature = req.headers["x-hookstream-signature"] ?? ""; const expected = "sha256=" + crypto .createHmac("sha256", process.env.HOOKSTREAM_SECRET) .update(rawBody) .digest("hex"); const a = Buffer.from(signature); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b);}
pythonimport hmac, hashlib, osdef verify_hookstream(request, raw_body: bytes) -> bool: signature = request.headers.get("x-hookstream-signature", "") expected = "sha256=" + hmac.new( os.environ["HOOKSTREAM_SECRET"].encode(), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(signature, expected)
goimport ( "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "os")func verifyHookstream(r *http.Request, rawBody []byte) bool { signature := r.Header.Get("X-hookstream-Signature") mac := hmac.New(sha256.New, []byte(os.Getenv("HOOKSTREAM_SECRET"))) mac.Write(rawBody) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected))}
Use a constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal) — not a plain == — so an attacker can't learn the signature byte-by-byte via timing side channels.