Webhook Signature Verification
How to verify webhook signatures to ensure events are from WarmySender.
Why Verify Signatures?
Without verification, anyone who discovers your endpoint URL could send fake events. Signature verification ensures only WarmySender can send valid events.
How It Works:
- When you create a webhook, you receive a secret (format: whsec_xxxx). Save it securely.
- Every webhook delivery includes an X-Warmy-Signature header.
- The header format is: t=<timestamp>,v1=<hex_signature>
- To verify, reconstruct the signature using your secret and compare.
Verification Steps:
Step 1: Extract timestamp and signature from X-Warmy-Signature header:
- Parse t= to get the timestamp (Unix milliseconds)
- Parse v1= to get the hex signature
Step 2: Build the signature payload:
- Concatenate: timestamp + "." + raw_request_body
- Example: "1710892810000.{"type":"reply.received",...}"
Step 3: Compute HMAC-SHA256:
- Use your webhook secret as the key
- Hash the signature payload from Step 2
- Get the hex digest
Step 4: Compare signatures:
- Use timing-safe comparison (NOT simple string equality)
- Reject if signatures do not match
Step 5 (Optional): Check timestamp freshness:
- Reject events older than 5 minutes to prevent replay attacks
- Compare: current_time - timestamp < 300000ms
Node.js Example:
const crypto = require('crypto');
function verify(secret, signatureHeader, body) {
const [tPart, vPart] = signatureHeader.split(',');
const timestamp = tPart.replace('t=', '');
const signature = vPart.replace('v1=', '');
const payload = timestamp + '.' + body;
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}
Python Example:
import hmac, hashlib
def verify(secret, signature_header, body):
parts = dict(p.split('=', 1) for p in signature_header.split(','))
payload = parts['t'] + '.' + body
expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(parts['v1'], expected)
Common Mistakes:
- Using the raw bytes of the body, not the UTF-8 string — always use the raw request body as a string.
- Using simple == comparison instead of timing-safe — vulnerable to timing attacks.
- Forgetting to save the webhook secret on creation — it is shown only once.
- Parsing/modifying the JSON body before verification — verify against the raw body, then parse.