Verify webhook signatures in Node, Python or PHP
Check the X-Hushdesk-Signature header so your endpoint only trusts events Hushdesk sent. Ready-to-copy code for Node.js, Python and PHP.
Last updated 2 min read
Anyone who learns your endpoint's address could send it fake events. The signature proves an event came from Hushdesk. Check it on every request before you act on the event.
Before you start
- Who this is for: developers running a webhook endpoint.
- Plan: all plans.
- What you need: the signing secret you typed when connecting the Webhooks app, stored in your server's environment (for example
HUSHDESK_WEBHOOK_SECRET).
Step by step
- Read the raw request body as bytes. Do not parse it yet.
- Compute
HMAC-SHA256(secret, rawBody)as lowercase hex and addsha256=in front. - Compare it with the
X-Hushdesk-Signatureheader using a constant-time comparison. - If they match, parse the JSON and handle the event. If not, answer
401. - Answer
200quickly, then do slow work in the background.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/helpdesk-events', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.HUSHDESK_WEBHOOK_SECRET).update(req.body).digest('hex');
const got = req.get('X-Hushdesk-Signature') || '';
const ok = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
// handle event.event and event.ticket here
});
Python (Flask)
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/helpdesk-events")
def helpdesk_events():
raw = request.get_data()
expected = "sha256=" + hmac.new(os.environ["HUSHDESK_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("X-Hushdesk-Signature", ""), expected):
abort(401)
event = request.get_json()
return "", 200
PHP
<?php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('HUSHDESK_WEBHOOK_SECRET'));
$got = $_SERVER['HTTP_X_HUSHDESK_SIGNATURE'] ?? '';
if (!hash_equals($expected, $got)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
http_response_code(200);
What happens next
Your endpoint only acts on genuine Hushdesk events. The Webhooks card in Hushdesk shows Last delivery as delivered when your endpoint answers 2xx, or failed with the status code when it does not.
Tips
- Keep the secret out of your code repository. Use environment variables.
- When you change the secret in Hushdesk with Update and test, deploy the new secret to your endpoint at the same time, or the test will fail.
- Use the
X-Hushdesk-Eventheader to route events before parsing the body.
Troubleshooting
"Your endpoint did not answer the test event with a 2xx"
Your code rejected the test ping, often because the signature was computed over a parsed body. Use the raw body.
"Use a signing secret of at least 16 characters"
Choose a longer secret before connecting.
Frequently asked questions
How is the Hushdesk webhook signature calculated?
Hushdesk takes the exact raw request body, computes an HMAC with SHA-256 using your signing secret as the key, writes it in lowercase hexadecimal and puts sha256= in front. That value is sent in the X-Hushdesk-Signature header. Your code repeats the same calculation and compares the two.
Why does my signature check fail even with the right secret?
Usually because the body was parsed and re-serialised before checking. Re-encoding JSON changes spacing or key order, which changes the signature. Always compute the HMAC over the raw bytes exactly as received, then parse the JSON only after the signature matches.
Should I compare signatures with a normal equals check?
Use a constant-time comparison instead, such as crypto.timingSafeEqual in Node, hmac.compare_digest in Python or hash_equals in PHP. A normal string comparison can leak, through timing, how many characters matched, which an attacker could use to guess a valid signature.
Is the test ping signed too?
Yes. The ping Hushdesk sends when you press Connect and test is signed with the same secret in the same way, so you can test your verification code before any real ticket events arrive. Return 200 only when the signature matches.
What should my endpoint do with an invalid signature?
Reject it with a 401 or 403 status and do nothing else. Do not log the full body of rejected requests. If valid events start failing, check that the secret in your code matches the one you saved, and update both together with Update and test.
Was this helpful?
Related articles
- Send ticket events with webhooksConnect the Webhooks app to post a signed JSON event to your HTTPS endpoint when a ticket is created, a customer replies or a ticket is closed.
- Webhook events and payload referenceThe exact JSON Hushdesk posts for ticket.created, customer.replied, ticket.closed and the test ping, with every field and header explained.
Still stuck?
Chat with the Hushdesk team. A person answers on every plan.