# Verify the Signature

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

Source: https://edesy.in/docs/missed-call/guides/verify-webhook-signature

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](https://edesy.in/tools/webhook-signature-verifier).

## Node.js (Express)

```js
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)

```python
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
<?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.
