Signature Verification
Every webhook request includes cryptographic headers so you can verify that the request genuinely originates from SIPSIM and has not been tampered with.
Signature Headers
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 hex digest of the signed payload |
X-Webhook-Timestamp | Unix timestamp (seconds) used in signing |
How It Works
The signature is computed as:
HMAC-SHA256(signing_secret, "{timestamp}.{raw_json_body}")
To verify a webhook:
- Extract the
X-Webhook-SignatureandX-Webhook-Timestampheaders - Reconstruct the signed string:
"{timestamp}.{raw_json_body}" - Compute the HMAC-SHA256 using your signing secret
- Compare your computed signature with the header value
- Reject the request if they don't match
Code Examples
- Python
- Node.js
- Ruby
import hmac
import hashlib
def verify_webhook(request_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
"""Verify a SIPSIM webhook signature."""
expected = hmac.new(
secret.encode(),
f"{timestamp}.{request_body.decode()}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your request handler:
# body = request.get_data()
# signature = request.headers.get("X-Webhook-Signature")
# timestamp = request.headers.get("X-Webhook-Timestamp")
# if not verify_webhook(body, signature, timestamp, "your_signing_secret"):
# return "Unauthorized", 401
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, timestamp, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// In your Express handler:
// app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// const signature = req.headers['x-webhook-signature'];
// const timestamp = req.headers['x-webhook-timestamp'];
// if (!verifyWebhook(req.body.toString(), signature, timestamp, 'your_signing_secret')) {
// return res.status(401).send('Invalid signature');
// }
// const event = JSON.parse(req.body);
// // process event...
// res.sendStatus(200);
// });
require 'openssl'
def verify_webhook(raw_body, signature, timestamp, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end
# In your controller:
# raw_body = request.raw_post
# signature = request.headers["X-Webhook-Signature"]
# timestamp = request.headers["X-Webhook-Timestamp"]
# unless verify_webhook(raw_body, signature, timestamp, "your_signing_secret")
# head :unauthorized and return
# end
Use Raw Body
Always use the raw request body (not a parsed-then-reserialized version) when computing the signature. Re-serializing JSON can change field order or whitespace, which will cause verification to fail.
Troubleshooting
| Problem | Solution |
|---|---|
| Signature never matches | Ensure you're using the raw request body, not a parsed/reserialized version |
| Signature matched before but stopped | Check if the signing secret was regenerated in the dashboard |
Next Steps
- Best Practices — Recommendations for reliable webhook processing
- Managing Webhooks via API — Programmatically manage event subscriptions