Next Restaurant 6.1.0: move your data in, run payroll, and connect other systems Next Inventory 6.2.0: a REST API and webhooks for your other systems Next DB 6.1: Auto-Updating Tables, Printing, Phone Layout and SEO Next Task 6.0.0: a task manager for Joomla Next Inventory 6.1.1: smoother Check Out, no more list limits, and much faster PHP2Shell: Is Your Joomla Site Safe? What to Do Today Introducing Next Boards: Kanban project and task boards for Joomla Building the Future of Joomla Extensions
NextSoftware

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 of webhook-id.webhook-timestamp.body, keyed with the secret's base64 part after whsec_, 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.

All Extensions