Verifying webhook signatures
Every delivery includes an X-ReachCell-Signature header formatted as sha256=<hex>. The signature is an HMAC-SHA256 over the raw request body.
The HMAC key is the SHA-256 hash of your webhook secret (shown once at webhook creation). To verify:
- Read the raw request body before parsing JSON.
- Compute
key = sha256(your_secret)as raw binary bytes. - Compute
HMAC-SHA256(raw_body, key)and hex-encode it. - Prefix with
sha256=and compare to the header using a timing-safe comparison.
If the signature does not match, reject the delivery with HTTP 400.
import hashlib, hmac
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
@app.post("/webhooks/reachcell")
def handle():
sig_header = request.headers.get("X-ReachCell-Signature", "")
raw_body = request.get_data()
key = hashlib.sha256(WEBHOOK_SECRET.encode()).digest()
expected = "sha256=" + hmac.new(key, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig_header):
abort(400, "Bad signature")
event = request.json
print(event["event_type"], event["data"])
return "", 200<?php
$secret = 'your_webhook_secret';
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_REACHCELL_SIGNATURE'] ?? '';
$key = hash('sha256', $secret, true); // raw binary
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $key);
if (!hash_equals($expected, $sigHeader)) {
http_response_code(400);
exit('Bad signature');
}
$event = json_decode($rawBody, true);
// $event['event_type'], $event['data']import crypto from 'crypto';
// Requires express.raw() middleware so req.rawBody is available
app.post('/webhooks/reachcell', (req, res) => {
const sig = req.headers['x-reachcell-signature'] ?? '';
const raw = req.rawBody;
const key = crypto.createHash('sha256')
.update(process.env.WEBHOOK_SECRET)
.digest(); // raw binary
const expected = 'sha256=' +
crypto.createHmac('sha256', key).update(raw).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
console.log(event.event_type, event.data);
res.sendStatus(200);
});