Webhook events
Outbound events. When stock or a record changes, Next Inventory sends an HTTP POST to each webhook subscribed to that event, so another system can react without polling.
A webhook says something happened; the REST API reads the full record. To keep another system's stock in step, subscribe to stock.changed: it covers every way stock can change.
Configured in Configuration → Webhooks.
Delivered as POST with Content-Type: application/json.
Envelope
{
"event": "stock.changed",
"timestamp": "2026-11-11T09:30:00Z",
"site": "https://your-site.example",
"data": []
}
| Field | Type | Meaning |
|---|---|---|
event |
string |
The event name. The same as the X-NextInventory-Event header. |
timestamp |
string |
ISO 8601, when the event happened. Unchanged on a retry. |
site |
string |
The sending site's root address, so one receiver can serve several sites. |
data |
object |
The event's own fields. |
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 delivery; verify either one: the Standard Webhooks
webhook-signature(base64, signs id, timestamp and body) orX-NextInventory-Signature(sha256=+ hex HMAC of the raw body only). - Each webhook has its own secret. A generated secret starts with
whsec_and is base64: the HMAC key is the decoded bytes after the prefix. A secret you typed yourself is used as-is. - Compute the HMAC over the bytes as received; re-encoding the parsed JSON changes them and the check fails. Compare with a constant-time function.
- Reject a
webhook-timestampmore than about 300 seconds old to refuse replayed messages.X-NextInventory-Signaturehas no timestamp, so it cannot tell a replay from a fresh delivery. - Making a new secret takes effect on the next delivery, so update the receiver first.
webhook-id(msg_<delivery id>) andX-NextInventory-Deliveryare the same on every retry of one delivery: dedupe on them.
Verify a Standard Webhooks signature in PHP
$body = file_get_contents('php://input');
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sigs = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
if (abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
$key = str_starts_with($secret, 'whsec_') ? base64_decode(substr($secret, 6)) : $secret;
$expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$ts.$body", $key, true));
$valid = false;
foreach (explode(' ', $sigs) as $sig) {
$valid = $valid || hash_equals($expected, $sig);
}
if (!$valid) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
Delivery
Retries: 7.
The Deliveries tab of the Webhooks screen lists every delivery with its event, status (pending, sent, failed), attempts, HTTP code and last error, with a status filter and Resend.
- Answer 2xx within 8 seconds. Anything else is retried after 1m, 5m, 30m, 2h, 6h, 12h and 24h; after the 8th failed attempt the delivery is marked failed. Answer
410 Goneto stop at once. - Be idempotent. Dedupe on
webhook-id: a retry or a Resend carries the same id. - Expect any order. A retry can arrive after a later event about the same item.
- Retries need the scheduled task "Next Inventory: Retry webhook deliveries", created when the first webhook is saved. The Health tab warns when deliveries are overdue.
- The endpoint must be reachable from your server. Private and local addresses are refused unless a Super User allows them.
- Ignore fields you do not recognise. New fields may be added over time.
Tick the events each webhook should receive. A webhook only receives the events it subscribes to; Send a test ping sends ping to it whatever it subscribes to.
Events
| Event | When |
|---|---|
stock.changed |
An item's stock quantity changed, in any way |
stock.low |
An item fell to its low-stock threshold |
stock_movement.finalized |
A check in, check out, add or remove batch was finalized |
transfer.finalized |
A warehouse transfer was finalized |
adjustment.finalized |
A stock adjustment was finalized |
item.created |
An item was created |
item.updated |
An item was edited |
item.deleted |
An item was deleted |
warehouse.created |
A warehouse was created |
warehouse.updated |
A warehouse was edited |
warehouse.deleted |
A warehouse was deleted |
recipient.created |
A recipient was created |
recipient.updated |
A recipient was edited |
recipient.deleted |
A recipient was deleted |
ping |
A test message |