Webhooks
Signature verification
Each delivery is signed with the endpoint's secret, so your server can tell El-B's requests from anyone else's.
The signature
X-ElB-Signature holds sha256= followed by the hexadecimal HMAC-SHA256 of the raw request body, keyed with the signing secret you received when you registered the endpoint or rotated its secret.
Sign the bytes you received, before any JSON parsing: re-encoding the body changes it (spacing, escaped characters) and the signature no longer matches.
Verifying it
$body = file_get_contents('php://input');
$expected = 'sha256='.hash_hmac('sha256', $body, getenv('ELB_WEBHOOK_SECRET'));
if (! hash_equals($expected, $_SERVER['HTTP_X_ELB_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// Answer at once; do the work in the background.
http_response_code(200); import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Keep the raw body: the signature covers its exact bytes.
app.post('/elb/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.ELB_WEBHOOK_SECRET).update(req.body).digest('hex');
const received = req.get('X-ElB-Signature') ?? '';
if (received.length !== expected.length || ! crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200);
}); import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/elb/webhooks")
def elb_webhook():
body = request.get_data()
expected = "sha256=" + hmac.new(os.environ["ELB_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-ElB-Signature", "")):
abort(401)
event = request.get_json()
return "", 200 Route::post('/elb/webhooks', function (Request $request) {
$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), config('services.elb.webhook_secret'));
abort_unless(hash_equals($expected, (string) $request->header('X-ElB-Signature')), 401);
// The same delivery can arrive twice: skip an id you already handled.
HandleElbEvent::dispatch($request->json()->all());
return response()->noContent();
}); Compare in constant time (hash_equals, timingSafeEqual, compare_digest), never with ==.
Rotating the secret
Rotate the signing secret returns a new secret that signs every delivery from that moment, retries included. Deploy it to your receiver right away.