Developer docs
Verify signatures
Make sure a webhook request really came from Bewtee.
On this page (3)
Anyone who learns your webhook address could send it fake requests. Every real request is signed with your webhook's signing secret, so check the signature before trusting the body.
How the signature is built
Bewtee-Signature: t=1794373200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
tis the Unix time the request was sent.v1is the hex HMAC-SHA256 of the string<t>.<raw request body>, keyed with your signing secret.
Steps
- Read the raw request body, before any JSON parsing. Re-serialised JSON will not match.
- Compute HMAC-SHA256 of
t + "." + rawBodywith your signing secret. - Compare it to
v1with a constant-time comparison. - Reject requests whose
tis more than 5 minutes old, to stop replays.
Node.js
const crypto = require('crypto');
function verifyBewteeSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((pair) => pair.split('=')));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: keep the raw body
app.post('/bewtee/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyBewteeSignature(req.body.toString('utf8'), req.get('Bewtee-Signature') || '', process.env.BEWTEE_WEBHOOK_SECRET);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // reply first, then do the work
handle(event);
});
PHP
function verifyBewteeSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
$parts = [];
foreach (explode(',', $header) as $pair) {
[$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
$parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
if ($timestamp === 0 || abs(time() - $timestamp) > $tolerance) {
return false;
}
return hash_equals(hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret), $parts['v1'] ?? '');
}
$rawBody = file_get_contents('php://input');
if (! verifyBewteeSignature($rawBody, $_SERVER['HTTP_BEWTEE_SIGNATURE'] ?? '', getenv('BEWTEE_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
http_response_code(200);
Python
import hashlib, hmac, time
def verify_bewtee_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(pair.split("=", 1) for pair in header.split(",") if "=" in pair)
try:
timestamp = int(parts.get("t", "0"))
except ValueError:
return False
if not timestamp or abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Rotating the secret
In the dashboard choose New signing secret on the webhook. The old secret stops working immediately, so update your server at the same time.