Verifying webhook signatures
Anyone who learns your endpoint URL can POST to it. Check the signature before you trust a message.
X-NextInventory-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request
body, keyed with the webhook's signing secret. Compute the same value from the bytes you
received, before parsing or re-encoding the JSON. Compare the two in constant time.
PHP
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('NEXTINVENTORY_WEBHOOK_SECRET'));
$signature = $_SERVER['HTTP_X_NEXTINVENTORY_SIGNATURE'] ?? '';
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);
Node.js
import crypto from 'node:crypto';
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.NEXTINVENTORY_WEBHOOK_SECRET)
.update(req.body).digest('hex');
const given = req.get('X-NextInventory-Signature') || '';
if (given.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
Python
import hashlib, hmac, os
def verified(raw_body: bytes, signature: str) -> bool:
expected = 'sha256=' + hmac.new(os.environ['NEXTINVENTORY_WEBHOOK_SECRET'].encode(),
raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or '')
Standard Webhooks
Every message also carries the Standard Webhooks headers, so you can use a ready-made library, which also refuses a replayed (old) message:
webhook-id:msg_<delivery id>, the same on every retry of one delivery.webhook-timestamp: when this attempt was sent, in Unix seconds.webhook-signature:v1,followed by the base64 HMAC-SHA256 ofwebhook-id.webhook-timestamp.body, keyed with the secret's base64 part afterwhsec_, decoded.
// npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.NEXTINVENTORY_WEBHOOK_SECRET); // the whsec_... secret
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = wh.verify(req.body, req.headers); // throws on a bad signature or an old timestamp
res.sendStatus(200);
} catch {
res.sendStatus(401);
}
});
A secret you typed yourself without the whsec_ prefix works for X-NextInventory-Signature;
for the Standard Webhooks libraries, use a generated one.
Rotating the secret
On the Endpoints tab, click Make a new secret next to the webhook. The new secret is shown once with a Copy button and applies to every message from then on, retries included: update your receiver at the same moment. You can also edit the webhook and type a new secret; leaving the field blank on an edit keeps the current one.