Skip to main content

Inventory webhooks

A signed message when stock, an order or a product changes.

Add an endpoint URL, pick its events, and keep the signing secret to check each delivery.

Works todayWebhooks work today on every plan.

Inventory webhook events

Stock, orders and products.

Pick the events each endpoint receives. Each delivery is one event, posted as JSON.

Webhook events
EventWhen
stock_level.updatedOn hand, reserved or available changed at a location.
stock_movement.createdA receipt, sale, transfer, count or adjustment posted.
product.createdA product or variant was added.
product.updatedA product or variant changed.
purchase_order.createdA purchase order was raised.
purchase_order.receivedStock on a purchase order was received.
sales_order.createdA sales order arrived, from any channel.
sales_order.updatedA sales order changed status or lines.
sales_order.shippedA sales order shipped in full or in part.
invoice.createdAn invoice was raised from a sales order.
sales_order.returnedA customer return was recorded against a sales order.
credit_note.createdA credit note was raised against an invoice.

Webhook payload example

Each event carries the record that changed.

The id is unique per event and the same on every retry, so you can skip one you have already handled. The data is the record as the API returns it.

A stock level event carries on hand, reserved, and on hand less reserved, each with its previous value. It fires on each movement, and when a sales order changes what is reserved.

stock_level.updated
{
  "id": "evt_demo_7c1e",
  "type": "stock_level.updated",
  "created_at": "2026-10-28T10:15:30Z",
  "data": {
    "variant_id": "demo-var-0001",
    "sku": "LTT-SGE",
    "location": {
      "id": "demo-loc-wh",
      "name": "Main warehouse"
    },
    "on_hand": 135,
    "reserved": 24,
    "available": 111,
    "previous_on_hand": 139,
    "previous_reserved": 28,
    "previous_available": 111,
    "cause": {
      "kind": "shipment",
      "movement_id": "demo-mv-006770",
      "sales_order_id": "demo-so-10891",
      "source": "scan"
    }
  }
}
sales_order.created
{
  "id": "evt_demo_7c2f",
  "type": "sales_order.created",
  "created_at": "2026-10-28T10:02:00Z",
  "data": {
    "id": "demo-so-10891",
    "number": "SO-10891",
    "status": "confirmed",
    "customer_id": "demo-cus-web",
    "total": "336.00",
    "currency": "CAD"
  }
}

Webhook signature verification

Check the signature before you trust a delivery.

Each endpoint has its own signing secret. The signature is an HMAC SHA-256 of the id, the timestamp and the raw body.

Delivery headers
POST /your/endpoint HTTP/1.1
Content-Type: application/json
Webhook-Id: evt_demo_7c1e
Webhook-Timestamp: 1793182530
Webhook-Signature: v1,Zm9yIGV4YW1wbGUgb25seSwgbm90IGEgcmVhbCBzaWduYXR1cmU=
Check a signature (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: the endpoint's signing secret, "whsec_" and base64, shown once when you add the endpoint
export function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64");
  return headers["webhook-signature"]
    .split(" ")
    .map((part) => part.replace(/^v1,/, ""))
    .some((sig) => sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}

Webhook retries

A failed delivery is tried again, with longer gaps.

Reply with any 2xx within 10 seconds. Every delivery and retry shows in the integration activity log in the app.

Retry schedule
RetryTime after the first failure
11 minute
25 minutes
330 minutes
42 hours
58 hours
61 day

Webhook test event

See a real signed payload before real events arrive.

Send test event, on the endpoint in the app or through the API, signs one sample of an event it takes. The body carries "test": true, and the activity log keeps the exact body and headers.

The sample is your newest matching record, or a fixed one when you have none. Leave out "event" to use the endpoint's first event. It is sent once with no retries, and the demo records it without sending.

Send a test event
POST /api/v1/webhook_endpoints/{id}/test
Authorization: Bearer <your API key>
Content-Type: application/json

{ "event": "sales_order.created" }
The answer
{
  "id": "evt_demo_7d40",
  "endpoint_id": "3f6c1b8e-0d2a-4c5e-9f71-2b8a6d4e1c90",
  "event": "sales_order.created",
  "test": true,
  "body": "{\"id\":\"evt_demo_7d40\",\"type\":\"sales_order.created\",\"created_at\":\"2026-10-28T10:20:00.000Z\",\"test\":true,\"data\":{\"id\":\"demo-so-10891\",\"number\":\"SO-10891\",\"status\":\"confirmed\"}}",
  "payload": {
    "id": "evt_demo_7d40",
    "type": "sales_order.created",
    "created_at": "2026-10-28T10:20:00.000Z",
    "test": true,
    "data": {
      "id": "demo-so-10891",
      "number": "SO-10891",
      "status": "confirmed"
    }
  },
  "headers": {
    "Content-Type": "application/json",
    "User-Agent": "webhooks/1",
    "Webhook-Id": "evt_demo_7d40",
    "Webhook-Timestamp": "1793182800",
    "Webhook-Signature": "v1,Zm9yIGV4YW1wbGUgb25seSwgbm90IGEgcmVhbCBzaWduYXR1cmU="
  },
  "result": {
    "sent": true,
    "recorded_only": false,
    "status": 200,
    "error": null
  },
  "activity_entry_id": "8a2d4f60-71c3-4b9e-a5d2-0e6f9c3b7a14"
}

Inventory management API pricing

The API, webhooks and the MCP server come with every plan.