# WMS outbound webhooks
URL: https://support.starshipit.com/articles/14700000001025-wms-outbound-webhooks
Canonical: https://support.starshipit.com/articles/14700000001025-wms-outbound-webhooks
Markdown: https://support.starshipit.com/articles/14700000001025-wms-outbound-webhooks.md
Updated: 2026-07-20

> For the complete documentation index, see [llms.txt](https://support.starshipit.com/llms.txt).

> Configure and secure WMS outbound webhooks, verify signatures, and handle supported events, payloads, retries, and ordering.

Use Starshipit WMS outbound webhooks to send signed warehouse event notifications to your HTTPS endpoint and process them safely in your own system.

## Understand the webhook direction

This guide covers events sent **from Starshipit WMS to an endpoint you control**. You configure these outbound webhooks in WMS under **System > Settings > Webhooks**.

They are different from the order and shipment events that Starshipit sends into WMS. Those inbound events keep WMS synchronised with Starshipit and are configured through the Starshipit WMS integration.

## Before you begin

You need:

- Permission to manage WMS settings.
- One publicly reachable HTTPS endpoint.
- A shared secret that both WMS and your receiver can store securely.
- A receiver that can access the unmodified raw request body before parsing JSON.
- Durable storage for received webhook IDs so your integration can ignore duplicate deliveries.

:::important
Do not expose the webhook secret in client-side code, logs, or error messages. Generate a long, random value and store it in your server-side secret manager.
:::

## Configure outbound webhooks

1. Sign in to [Starshipit WMS](https://wms.starshipit.com).
2. Go to **System > Settings**.
3. Select **Webhooks**.
4. Enter your public HTTPS endpoint in **Webhook URL**.
5. Enter your shared signing secret in **Webhook secret**.
6. Enable the event groups your endpoint should receive.
7. Turn on **Enable webhooks**.
8. Select **Save changes**.

You must configure a URL, a secret, and at least one event group before you can enable webhooks. After a secret is saved, WMS does not display it again. Leave **Webhook secret** blank when saving later changes if you want to keep the stored secret.

WMS currently has no test-send action. To verify your setup, enable only the event group you want to test, perform a low-risk action with test data, and confirm the delivery in both your receiver logs and WMS monitoring.

## Meet the endpoint security requirements

WMS accepts a webhook URL only when it meets these rules:

- The URL uses `https://`.
- The URL uses a public, fully qualified hostname or a public IPv4 address. `localhost`, `.localhost`, `.local`, single-label hostnames, and IPv6 literal URLs are not accepted. A hostname can resolve to public IPv6 addresses.
- The URL does not contain a username or password.
- Every address returned for a hostname must pass WMS address checks. For IPv4, WMS blocks unspecified, private, loopback, carrier-grade NAT, link-local, benchmark, multicast, and reserved ranges. For IPv6, WMS blocks unspecified, loopback, unique-local, link-local, site-local, multicast, and mapped blocked IPv4 addresses.
- The endpoint returns a final response directly. WMS does not follow HTTP redirects.

WMS resolves the hostname again when it sends a request. A URL that was valid when saved can still be blocked at delivery time if its DNS records later resolve to a non-public address.

## Receive a webhook request

WMS sends an HTTP `POST` request with a UTF-8 JSON body. The body uses this envelope:

```json
{
  "id": "inventory-changed:account-123:movement-456",
  "event_type": "inventory_changed",
  "occurred_at_utc": "2026-07-20T02:15:30.000Z",
  "source": "starshipit-wms",
  "account_id": "account-123",
  "data": {
    "stock_movement_id": "movement-456",
    "product_id": "product-789",
    "location_id": "location-101",
    "from_location_id": null,
    "inventory_id": "inventory-202",
    "client_id": null,
    "change": -2,
    "reason": "Order shipped",
    "reference": "order-1001",
    "timestamp_utc": "2026-07-20T02:15:30.000Z",
    "batch_number": null,
    "serial_number": null,
    "expiry_date_utc": null
  }
}
```

| Envelope field | Type | Description |
| --- | --- | --- |
| `id` | string | Stable event ID. It stays the same across retries of the same event. |
| `event_type` | string | One of the supported event names in this guide. |
| `occurred_at_utc` | string | ISO 8601 timestamp for the event occurrence. |
| `source` | string | Always `starshipit-wms`. |
| `account_id` | string | WMS account ID that owns the event. |
| `data` | object | Event-specific payload. |

### Request headers

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `Content-Length` | Byte length of the UTF-8 request body. |
| `User-Agent` | `starshipit-wms` |
| `X-Starshipit-WMS-Webhook-Id` | Same value as body `id`. |
| `X-Starshipit-WMS-Webhook-Event` | Same value as body `event_type`. |
| `X-Starshipit-WMS-Timestamp` | Delivery timestamp used when calculating the signature. |
| `X-Starshipit-WMS-Signature` | `sha256=` followed by the lowercase hexadecimal HMAC digest. |

Header names are case-insensitive. After verifying the signature, confirm the ID and event headers match their corresponding body fields.

## Verify the signature

WMS signs the exact raw request body with HMAC-SHA256. It builds the signed value as:

```text
X-Starshipit-WMS-Timestamp + "." + raw_request_body
```

It then calculates the HMAC with your webhook secret and sends the hexadecimal result as:

```text
sha256=<hexadecimal_digest>
```

Read the body as raw bytes or an unmodified UTF-8 string. Do not parse and re-serialise the JSON before verification. Whitespace, escaping, and property order affect the digest.

### Node.js verification example

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWmsWebhook({ secret, timestamp, rawBody, signature }) {
  if (!timestamp || !/^sha256=[0-9a-f]{64}$/.test(signature ?? "")) {
    return false;
  }

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");
  const received = signature.slice("sha256=".length);

  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex"),
  );
}
```

Call the function before `JSON.parse(rawBody)`. Pass the value of `X-Starshipit-WMS-Timestamp` as `timestamp` and `X-Starshipit-WMS-Signature` as `signature`.

### Python verification example

```python
import hashlib
import hmac
import re


def verify_wms_webhook(secret, timestamp, raw_body, signature):
    if not timestamp or not re.fullmatch(r"sha256=[0-9a-f]{64}", signature or ""):
        return False

    signed_value = timestamp.encode("utf-8") + b"." + raw_body
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        signed_value,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)
```

Pass the original request body bytes as `raw_body`. Verify the signature before decoding or parsing the JSON.

For additional replay protection, validate that the delivery timestamp is a valid ISO 8601 timestamp and falls within a time window your service accepts. Use the stable event ID for deduplication; do not use the delivery timestamp as the deduplication key because it changes on each attempt.

## Acknowledge deliveries safely

WMS treats any `2xx` response as a successful delivery. Your endpoint has five seconds to respond.

Use this receiver pattern:

1. Read the raw request body.
2. Verify the HMAC signature with a constant-time comparison.
3. Validate the envelope and confirm the header ID and event match the body.
4. Atomically store the event ID and payload, or recognise that the ID was already stored.
5. Return a `2xx` response.
6. Process slower downstream work asynchronously.

Return a non-`2xx` response if you could not durably accept a valid event. WMS retries request errors, timeouts, and non-`2xx` responses.

## Handle retries, duplicates, and ordering

WMS uses at-least-once delivery. Your endpoint can receive the same event more than once, including when WMS sent the first request successfully but did not receive the response.

### Deduplicate by event ID

The body `id` and `X-Starshipit-WMS-Webhook-Id` stay stable across delivery attempts for the same event. Store this ID in a table with a unique constraint and make the event handler idempotent.

Treat the event ID as an opaque string. Its internal format can differ by event type and should not drive your business logic.

Do not deduplicate using `occurred_at_utc`, `X-Starshipit-WMS-Timestamp`, or a hash of the complete request. The delivery timestamp and signature change on every attempt.

### Delivery attempts and backoff

WMS makes up to five attempts in total. After a failed attempt, it schedules the next attempt with exponential backoff and bounded jitter:

| Failed attempt | Approximate delay before the next attempt |
| --- | --- |
| 1 | 1 minute, with ±25% jitter. |
| 2 | 2 minutes, with ±25% jitter. |
| 3 | 4 minutes, with ±25% jitter. |
| 4 | 8 minutes, with ±25% jitter. |

Retry delays are capped at 15 minutes. After the fifth failed attempt, the delivery moves to a terminal failed state and WMS does not attempt it again automatically.

### Per-entity ordering

WMS preserves first-in, first-out processing within these delivery streams:

| Event family | Ordering stream |
| --- | --- |
| `job_created`, `job_status_changed` | WMS job ID. |
| Purchase order lifecycle events and `stock_received` | Purchase order ID. |
| Order lifecycle events | WMS order ID. |
| `inventory_changed` | Product ID and location ID pair. |
| `low_stock_alert` | Product ID. |

Events in different streams can be delivered concurrently. There is no account-wide or global ordering guarantee. Separate streams for the same product, such as `inventory_changed` and `low_stock_alert`, are not ordered relative to each other.

## Supported events

You enable related events as groups in WMS settings, but each request contains one exact event name.

| Event name | WMS setting | Sent when a covered WMS workflow records |
| --- | --- | --- |
| `low_stock_alert` | **Low stock alerts** | A product crosses from a non-urgent restocking state into urgent restocking status. |
| `stock_received` | **Stock received** | A purchase order receipt records a positive received quantity. |
| `inventory_changed` | **Inventory changed** | A stock movement is created. |
| `purchase_order_created` | **Purchase order lifecycle** | A purchase order is created. |
| `purchase_order_updated` | **Purchase order lifecycle** | A purchase order is updated without changing status. |
| `purchase_order_status_changed` | **Purchase order lifecycle** | A purchase order moves to a different status. |
| `purchase_order_deleted` | **Purchase order lifecycle** | A purchase order is deleted. |
| `job_created` | **Job created** | A WMS job is created. |
| `job_status_changed` | **Job status changed** | A WMS job moves to a different status. |
| `order_created` | **Order lifecycle** | A WMS order record is created. |
| `order_updated` | **Order lifecycle** | A covered WMS order update is recorded. |
| `order_status_changed` | **Order lifecycle** | A WMS order moves to a different status. |
| `order_deleted` | **Order lifecycle** | A WMS order is deleted or archived through a covered deletion workflow. |

:::important
Lifecycle webhooks are operational notifications, not a complete change-data-capture or audit feed. Some imports, synchronisations, bulk workflows, and administrative actions can change records without emitting an outbound lifecycle event. Build periodic API reconciliation for data that must be complete.
:::

## Event payload reference

The following tables describe the object inside the envelope `data` field. Fields described as nullable are present with `null` when no value is available. Fields described as optional can be omitted.

### `low_stock_alert`

| Field | Type | Description |
| --- | --- | --- |
| `product_id` | string | WMS product ID. |
| `product_name` | string | Product name. |
| `sku` | string | Product SKU. |
| `available_quantity` | integer | Stock quantity used by the alert calculation, rounded to a non-negative whole number. |
| `days_until_stockout` | number | Projected days until the product is out of stock. |
| `lead_time_days` | number | Product lead time in days. |

This event is emitted only when WMS detects a transition into urgent restocking status with a valid stockout projection and a positive lead time. It is not emitted repeatedly while the product remains urgent.

### `stock_received`

| Field | Type | Description |
| --- | --- | --- |
| `purchase_order_id` | string | WMS purchase order ID. |
| `purchase_order_number` | string | Purchase order number. |
| `supplier_name` | string | Supplier name. |
| `received_at_utc` | string | ISO 8601 receipt timestamp. |
| `total_quantity_received` | number | Total quantity received in this receiving action. |
| `line_items` | array | Items and locations included in this receiving action. |

Each `line_items` entry contains:

| Field | Type | Description |
| --- | --- | --- |
| `product_id` | string | WMS product ID. |
| `product_name` | string | Product name. |
| `sku` | string | Product SKU. |
| `location_id` | string | Destination WMS location ID. |
| `location_name` | string | Destination location name. |
| `quantity_received` | number | Quantity received for this product and location. |

### `inventory_changed`

| Field | Type | Description |
| --- | --- | --- |
| `stock_movement_id` | string | Stable WMS stock movement ID. |
| `product_id` | string | WMS product ID. |
| `location_id` | string | WMS location affected by the movement. |
| `from_location_id` | string or null | Source location for a transfer, when available. |
| `inventory_id` | string or null | Related WMS inventory record ID. |
| `client_id` | string or null | Related 3PL client ID. |
| `change` | number | Signed quantity change. Positive values add stock and negative values remove stock. |
| `reason` | string | WMS movement reason. |
| `reference` | string | WMS reference recorded for the movement. |
| `timestamp_utc` | string | ISO 8601 movement timestamp. |
| `batch_number` | string or null | Batch number for batch-tracked stock. |
| `serial_number` | string or null | Serial number for serial-tracked stock. |
| `expiry_date_utc` | string or null | ISO 8601 expiry date or timestamp for tracked stock. |

### Purchase order lifecycle events

The following events use the same `data` shape:

- `purchase_order_created`
- `purchase_order_updated`
- `purchase_order_status_changed`
- `purchase_order_deleted`

| Field | Type | Description |
| --- | --- | --- |
| `purchase_order_id` | string | WMS purchase order ID. |
| `purchase_order_number` | string | Purchase order number. |
| `supplier_id` | string or null | WMS supplier ID. |
| `supplier_name` | string or null | Supplier name. |
| `previous_status` | string or null | Previous status for a status change or deletion; otherwise `null`. |
| `status` | string | Current or last recorded purchase order status. |
| `created_date_utc` | string | ISO 8601 purchase order creation date. |
| `expected_arrival_date_utc` | string or null | ISO 8601 expected arrival date. |
| `cartons_received` | number or null | Number of cartons received. |
| `line_items` | array | Purchase order line items. |

Purchase order `status` values are `DRAFT`, `CREATED`, `PARTIALLY_RECEIVED`, and `RECEIVED`.

Each `line_items` entry contains:

| Field | Type | Description |
| --- | --- | --- |
| `line_item_id` | string or null | WMS purchase order line item ID. |
| `product_id` | string | WMS product ID. |
| `quantity` | number | Line quantity. |
| `quantity_ordered` | number or null | Quantity ordered. |
| `quantity_received` | number or null | Quantity received. |
| `price` | number or null | Line item price. |

### `job_created`

The `data` object contains the job fields below.

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | WMS job ID. |
| `job_type` | string | WMS job type. |
| `status` | string | Current job status. |
| `priority` | number | Job priority. |
| `display_name` | string or null | Job display name. |
| `created_at_utc` | string | ISO 8601 creation timestamp. |
| `updated_at_utc` | string | ISO 8601 last update timestamp. |
| `started_at_utc` | string or null | ISO 8601 start timestamp. |
| `completed_at_utc` | string or null | ISO 8601 completion timestamp. |
| `paused_at_utc` | string or null | ISO 8601 pause timestamp. |
| `assigned_user_id` | string or null | Assigned WMS user ID. |
| `order_ids` | array, optional | Related WMS order IDs. |
| `purchase_order_id` | string or null | Related WMS purchase order ID. |
| `packing_location_id` | string or null | Related packing location ID. |
| `staging_location_id` | string or null | Related staging location ID. |
| `zone_id` | string or null | Related WMS zone ID. |
| `replenishment_product_id` | string or null | Product ID for a replenishment job. |
| `replenishment_location_id` | string or null | Location ID for a replenishment job. |
| `replenishment_quantity` | number or null | Quantity for a replenishment job. |

Job `job_type` values are `PICK`, `PACK`, `SHIP`, `PUTAWAY`, `REPLENISHMENT`, `KITTING`, and `CYCLE_COUNT`. Job `status` values are `PENDING`, `READY`, `ASSIGNED`, `IN_PROGRESS`, `PAUSED`, `COMPLETED`, and `FAILED`.

### `job_status_changed`

| Field | Type | Description |
| --- | --- | --- |
| `previous_status` | string or null | Status before the change. |
| `new_status` | string | Status after the change. |
| `job` | object | Complete job object using the `job_created` fields above. |

WMS does not send this event when a job update leaves the status unchanged. There are no separate `job_updated` or `job_deleted` outbound events.

### Order lifecycle events

The following events use the same `data` shape:

- `order_created`
- `order_updated`
- `order_status_changed`
- `order_deleted`

| Field | Type | Description |
| --- | --- | --- |
| `order_id` | string | WMS order ID. |
| `starshipit_order_id` | string | Related Starshipit order ID. |
| `order_number` | string | Customer-facing order number. |
| `reference` | string | Order reference. |
| `client_id` | string or null | Related 3PL client ID. |
| `account_id_override` | number or null | Starshipit account override value, when used. |
| `previous_status` | string or null | Status before a status change or deletion; otherwise `null`. |
| `status` | string | Current or last recorded WMS order status. |
| `delete_reason` | string or null | `order_deleted` or `order_archived` for a deletion event; otherwise `null`. |
| `order_date_utc` | string | ISO 8601 order date. |
| `created_at_utc` | string | ISO 8601 WMS record creation timestamp. |
| `updated_at_utc` | string | ISO 8601 WMS record update timestamp. |
| `line_items` | array, optional | WMS order line items when the emission path has loaded them. |

Order `status` values are `PENDING`, `RELEASED`, `PARTIALLY_SHIPPED`, `SHIPPED`, `CANCELLED`, and `RETURNED`.

Each `line_items` entry contains:

| Field | Type | Description |
| --- | --- | --- |
| `line_item_id` | string or null | WMS order line item ID. |
| `product_id` | string | WMS product ID. |
| `quantity` | number | Ordered quantity. |

## Monitor webhook delivery

Check both WMS and your receiver when you monitor the integration.

### Check queue activity

1. Open the WMS **Operations** dashboard.
2. Select **Background work**.
3. Find entries labelled **Webhook delivery**.
4. Review the status, attempt count, error, and next retry time.

A `completed` status means the configured endpoint returned a `2xx` response. A `dead letter` status means all five attempts failed.

### Check process logs

Go to **System > Process Logs** and filter for webhook activity. WMS records entries such as:

- `WMS webhook emitted: <event_name>` for successful delivery.
- `WMS webhook delivery failed: <event_name>` for a failed attempt.
- `WMS webhook not emitted: <event_name>` when an unexpected configuration or event condition prevents emission.

Process log details include the event ID, event type, occurrence time, destination hostname, and response status or failure reason where available. They do not expose the webhook secret.

### Log deliveries in your receiver

Record at least the event ID, event type, account ID, occurrence time, receive time, deduplication result, and downstream processing status. Do not log the shared secret or other sensitive data from your environment.

## Troubleshoot outbound webhooks

| Problem | What to check |
| --- | --- |
| No webhook is queued | Confirm webhooks and the correct event group are enabled. Confirm the action uses a covered emission path and meets event-specific conditions. |
| The URL cannot be saved | Use HTTPS with a public, fully qualified hostname and remove embedded credentials. |
| Delivery is blocked as a private address | Check every IPv4 and IPv6 address returned by public DNS. Remove private, local, loopback, link-local, or other non-public results. |
| Signature verification fails | Use the exact raw body and the delivery timestamp header. Do not parse, format, or re-serialise JSON first. Confirm both systems use the same secret. |
| The endpoint receives duplicates | Deduplicate atomically on the stable event ID and make downstream handlers idempotent. |
| Deliveries time out | Durably accept the event and return `2xx` within five seconds. Move slow work to your own queue. |
| Events appear out of order | Compare their ordering streams. Ordering is per entity stream, not global across an account or between different event families. |
| A delivery reached `dead letter` | Fix the endpoint, then reconcile the affected data through the WMS API. Customer self-service replay is not available. |

## Current limitations

- You can configure one outbound webhook endpoint per WMS account.
- WMS does not provide a test-send action.
- Customers cannot replay a delivery from WMS.
- The external webhook envelope has no `schema_version` field or schema-version header.
- Lifecycle webhooks do not cover every possible mutation path and are not a complete audit feed.
- WMS provides per-entity ordering, not global event ordering.

Because the external envelope has no schema version, ignore unknown fields and avoid rejecting a valid event only because WMS added a field you do not use.

## Related guides

- [WMS API guide](/articles/developer-center/starshipit-wms-api/wms-api-guide)
- [WMS API fundamentals](/articles/developer-center/starshipit-wms-api/wms-api-fundamentals)
- [WMS Settings reference](/articles/14700000000031-starshipit-wms-settings-reference)
- [Running WMS alongside Starshipit](/articles/14700000000004-starshipit-wms-working-alongside-starshipit-hybrid-outbound-headless-mode)
