Webhook events
Outbound events. When something happens in Next Restaurant, an HTTP POST is sent to the endpoints you configure — so another system can react without polling.
Webhooks tell you an order changed; they do not carry the whole order. There is no endpoint to fetch an order back, so keep the response you got when you created it.
Configured in Integrations.
Delivered as POST with Content-Type: application/json.
Envelope
{
"id": "msg_4e9c1b7a2f08d3516ac4be9017d2f5a3",
"type": "order.placed",
"event": "order.placed",
"data": [],
"timestamp": "2026-08-28T19:30:00+00:00"
}
| Field | Type | Meaning |
|---|---|---|
id |
string |
Unique id for this delivery. The same value as the webhook-id header — dedupe on it. |
type |
string |
The event name. |
event |
string |
The same name again, kept for integrations built before type existed. |
data |
object |
Identifiers for what changed — not the whole order. |
timestamp |
string |
ISO 8601, when the event was dispatched. |
Signing
Each delivery carries webhook-signature in the form v1,<base64>, computed with HMAC-SHA256.
The signature covers the {webhook-id}.{webhook-timestamp}.{raw body}.
- Two schemes are sent on every signed delivery and you verify either one, not both: the preferred
webhook-signature(Standard Webhooks, base64) and the legacyX-NextRestaurant-Signature(sha256=+ hex over the raw body only). - An endpoint URL is not a secret — it can be guessed, logged by a proxy or leak from a config file. Without verification, anyone who learns the URL can post fabricated orders to your integration.
- Verify before parsing or acting on the body, not after. Compute the HMAC over the bytes as received: re-serialising the JSON first changes them and the check will fail. Compare with a constant-time function.
- Reject a delivery whose
webhook-timestampis more than ~300 seconds old, before checking the signature. The legacy scheme has no timestamp in what it signs, so it cannot tell a replay from a fresh delivery. - A
whsec_-prefixed secret is base64: the HMAC key is the decoded bytes. A hand-typed secret is used as-is. - With no secret set, deliveries go out unsigned — both signature headers are simply absent, while
webhook-idandwebhook-timestampare still sent. This is the state a new install ships in.
Verify a Standard Webhooks signature in PHP
$body = file_get_contents('php://input');
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
// Reject stale deliveries before doing any crypto.
if (!$ts || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
$secret = str_starts_with($MY_SECRET, 'whsec_')
? base64_decode(substr($MY_SECRET, 6))
: $MY_SECRET;
$expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$ts.$body", $secret, true));
$ok = false;
foreach (explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '') as $candidate) {
// Several signatures may be present during a key rotation.
if (hash_equals($expected, $candidate)) {
$ok = true;
}
}
if (!$ok) {
http_response_code(401);
exit;
}
Delivery
There are no retries. A delivery that fails is not repeated.
Integrations shows recent delivery attempts — whether an endpoint received an event, how it responded, whether it was signed and under which schemes, and the delivery id. It is the first place to look when an integration "didn't get" something.
- There are no retries. One attempt per endpoint with a five-second timeout. An endpoint that is down, slow, or answers outside the 2xx range loses that event permanently — treat webhooks as a fast path, not a guaranteed ledger, and reconcile from your own records if a gap would matter.
- Be idempotent. Dedupe on
webhook-id, which is stable for one event across every endpoint it is sent to, so a consumer subscribed twice can tell the second copy is not a second event. - Respond quickly. Acknowledge receipt and do the slow work asynchronously.
- The endpoint must be reachable from your server over HTTPS. A URL on a private network your Joomla host cannot reach will never receive anything.
- Ignore fields you do not recognise. New fields are added to payloads over time.
Select only the events you actually consume. Every selected event is delivered to every configured endpoint. Selecting none means all of them, not none.
Events
| Event | When |
|---|---|
order.placed |
A new order is created, on any channel |
order.updated |
An existing order changes |
order.status_changed |
An order moves to a new status |
order.cancelled |
An order is cancelled, with the reason |
ping |
You press Test webhook — for connectivity checks only |