PHP2Shell: Is Your Joomla Site Safe? What to Do Today Introducing Next Boards: Kanban project and task boards for Joomla Next Exams 6.2.0: Exam Studio, six visual question types, exam embeds and six new languages Building the Future of Joomla Extensions
NextSoftware

Verifying signatures

Verifying deliveries are genuine

Set a signing secret in Integrations and verify it on your side before acting on a payload.

An endpoint URL is not a secret — it can be guessed, logged by a proxy, or leak from a config file. Without signature verification, anyone who learns the URL can post fabricated orders to your integration. With it, a forged request fails the check and is discarded.

Verify the signature before parsing or acting on the body, not after.

What every delivery carries

Header Example Always sent?
webhook-id msg_4e9c1b7a2f08d3516ac4be9017d2f5a3 Yes
webhook-timestamp 1788112371 (Unix seconds) Yes
webhook-signature v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= Only when a secret is set
X-NextRestaurant-Event order.placed Yes
X-NextRestaurant-Signature sha256=6f2b… Only when a secret is set

Two signature schemes go out on every signed delivery, over the same body with the same key. Verify either one — not both.

Standard Webhooks (use this for new integrations)

webhook-signature follows the Standard Webhooks specification, so any library implementing it will verify our deliveries with no custom code.

The signed content is the three parts joined by full stops:

{webhook-id}.{webhook-timestamp}.{raw body}

The signature is the base64 of the HMAC-SHA256 of that string — base64, not hex, which is the one difference from the legacy header below.

$body = file_get_contents('php://input');
$id   = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$ts   = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';

// Reject anything stale BEFORE checking the signature. A signature stays valid
// forever, so without this a delivery someone captured can be replayed at you
// indefinitely and it will verify perfectly every time.
if (!$ts || abs(time() - (int) $ts) > 300) {
    http_response_code(400);
    exit;
}

$expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$ts.$body", $YOUR_KEY, true));

// The header is a space-delimited list so a key rotation can send more than one.
// Split it and accept if any entry matches — do not compare the whole header.
$sent = explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '');
$ok   = false;

foreach ($sent as $candidate) {
    if (hash_equals($expected, $candidate)) {
        $ok = true;
    }
}

if (!$ok) {
    http_response_code(401);
    exit;
}

Your signing secret

Generate secret mints one in the form whsec_ followed by base64. For a whsec_ secret the HMAC key is the decoded bytes, not the string you can see — that is what Standard Webhooks libraries expect, and they handle the decoding for you.

If your secret was typed in by hand rather than generated, it is used as-is: the raw bytes of the literal string. Nothing about existing secrets changed.

Rotating the secret invalidates signatures under both schemes, which is what rotation means. Update your consumer at the same time.

The original header

X-NextRestaurant-Signature is unchanged and stays supported. Its value is sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your signing secret. Compute it over the bytes as received — re-serialising the JSON first will change them and the check will fail.

$body     = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $YOUR_SECRET);

if (!hash_equals($expected, $_SERVER['HTTP_X_NEXTRESTAURANT_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

This scheme has no timestamp in what it signs, so it cannot tell a replay from a fresh delivery on its own. That is the reason to prefer the Standard Webhooks header.

With no secret set, deliveries go out unsigned

Both signature headers are simply absent — webhook-id and webhook-timestamp are still sent — and a consumer that verifies will reject every delivery. This is the state a new install ships in. Generate the secret before you write the verification, and check the delivery log below to confirm what is actually being sent.

All Extensions