Webhook Signatures and Security
Learn how ACS signs every outbound webhook with HMAC-SHA256 and how to verify payloads to ensure requests are authentic.
Updated April 8, 2026
Every outbound webhook ACS sends includes an X-ACS-Signature header. This signature lets your receiving endpoint verify that the request genuinely came from ACS and that the payload has not been tampered with in transit.
How Signatures Work
ACS signs each webhook payload using HMAC-SHA256 with the unique secret generated when you created the webhook destination. The signature is computed over the raw request body bytes and delivered in the header:
X-ACS-Signature: sha256=<hex_digest>
To verify, your endpoint recomputes the HMAC using the same secret and compares it to the value in the header. A match confirms the request is authentic and unmodified.
Finding Your Webhook Secret
Your HMAC signing secret is displayed when you first create a webhook destination in Settings → Integrations → Outbound Webhooks. If you need to retrieve or rotate it later, find your destination in that list and click View Secret or Rotate Secret.
Verification Code Examples
Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Express route example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-acs-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
// process payload...
res.status(200).send('OK');
});
Python (Flask)
import hmac, hashlib, os
from flask import request, abort
def verify_webhook(raw_body, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhook', methods=['POST'])
def webhook():
sig = request.headers.get('X-ACS-Signature', '')
if not verify_webhook(request.get_data(), sig, os.environ['WEBHOOK_SECRET']):
abort(401)
payload = request.get_json()
# process payload...
return 'OK', 200
PHP
function verifyWebhook(string $body, string $signature, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
return hash_equals($expected, $signature);
}
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_ACS_SIGNATURE'] ?? '';
if (!verifyWebhook($body, $sig, getenv('WEBHOOK_SECRET'))) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($body, true);
// process payload...
Critical: Use the Raw Request Body
Always verify against the raw, unprocessed request body — before any JSON parsing, decoding, or reformatting. Parsing and re-serializing JSON can alter whitespace or key order, causing the signature check to fail even on legitimate requests. In Express, use express.raw() middleware instead of express.json() for the webhook route.
Use Timing-Safe Comparison
Use a constant-time comparison function when comparing signature strings:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals()
A simple === comparison is vulnerable to timing attacks that can allow an attacker to guess valid signatures.
Zapier and Make
If you are delivering webhooks to Zapier or Make, you generally do not need to implement signature verification — Zapier and Make use their own authentication at the trigger level. You can safely skip this step for those integrations.
Rotating Your Webhook Secret
If your secret is accidentally exposed:
- Go to Settings → Integrations → Outbound Webhooks
- Find the affected destination and click Rotate Secret
- Copy and store the new secret securely
- Update your receiving endpoint with the new value
ACS begins using the new secret immediately for all subsequent deliveries. In-flight deliveries using the old secret will fail verification on your endpoint — this is the expected and correct behavior.