When your webhook has a secret, every request carries:
X-Edesy-Signature: sha256=<hex>
<hex> is the HMAC-SHA256 of the raw request body, keyed with your secret, hex encoded. To verify:
- Read the body as raw bytes, before any JSON parsing. Re-serialising parsed JSON changes the bytes and the check fails.
- Compute
"sha256=" + hex(HMAC_SHA256(secret, raw_body)). - Compare it with the header using a constant-time comparison. Reject the request (for example with 401) if it does not match.
You can check a sample body, secret and header in the browser with the webhook signature verifier.
Node.js (Express)#
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.EDESY_WEBHOOK_SECRET;
app.post(
"/webhooks/missed-call",
express.raw({ type: "application/json" }), // keep the raw body
(req, res) => {
const expected =
"sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const received = req.get("X-Edesy-Signature") || "";
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // reply first, work after
if (event.event === "call.missed") {
// queue event.data.from, event.data.call_sid ...
}
}
);
Python (Flask)#
import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["EDESY_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/missed-call")
def missed_call():
raw = request.get_data() # raw bytes
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
received = request.headers.get("X-Edesy-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
event = request.get_json()
if event["event"] == "call.missed":
pass # queue event["data"]["from"], event["data"]["call_sid"] ...
return "", 200
PHP#
<?php
$secret = getenv('EDESY_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // raw body
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
$received = $_SERVER['HTTP_X_EDESY_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($raw, true);
if ($event['event'] === 'call.missed') {
// queue $event['data']['from'], $event['data']['call_sid'] ...
}
http_response_code(200);
Notes#
- The
webhook.testevent from the Send test button is signed the same way, so use it to check your code. - If you change the secret, update your server at the same time. Deliveries are signed with the secret saved when they are sent.
- Without a secret, the header is not sent. Then treat your URL itself as a credential: use a long, unguessable path.