Verify the Signature

Verify the X-Edesy-Signature HMAC-SHA256 header on missed call webhooks in Node.js, Python and PHP.

1 min read

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:

  1. Read the body as raw bytes, before any JSON parsing. Re-serialising parsed JSON changes the bytes and the check fails.
  2. Compute "sha256=" + hex(HMAC_SHA256(secret, raw_body)).
  3. 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.test event 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.
Was this page helpful? Suggest an edit