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
const 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);
}import hmac, hashlib, os
def 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)import (
"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.