Webhooks
Signature Verification
Each delivery is signed with the endpoint's secret, so your server can tell Rassmi's requests from anyone else's.
The signature
X-Rassmi-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('RASSMI_WEBHOOK_SECRET'));
if (! hash_equals($expected, $_SERVER['HTTP_X_RASSMI_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// Answer fast; queue the work.
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('/hooks/rassmi', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.RASSMI_WEBHOOK_SECRET).update(req.body).digest('hex');
const received = req.get('X-Rassmi-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("/hooks/rassmi")
def rassmi_webhook():
body = request.get_data()
expected = "sha256=" + hmac.new(os.environ["RASSMI_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Rassmi-Signature", "")):
abort(401)
event = request.get_json()
return "", 200
Route::post('/hooks/rassmi', function (Request $request) {
$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), config('services.rassmi.webhook_secret'));
abort_unless(hash_equals($expected, (string) $request->header('X-Rassmi-Signature')), 401);
ProcessRassmiEvent::dispatch($request->json()->all());
return response()->noContent();
});
Rotating the secret
Rotate Signing Secret returns a new secret that signs every delivery from that moment. Deploy it to your receiver right away; deliveries still being retried are signed with the new secret too.