Webhook Service Documentation
Webhook Service
The Shelfless Webhook Service allows you to receive real-time notifications about events in your Shelfless integration. By subscribing to webhooks, your application can automatically receive updates about order statuses, inventory changes, and other important events.
Getting Started
- Create a webhook endpoint in your application that can receive POST requests
- Subscribe to events using the Shelfless API
- Verify webhook signatures to ensure message authenticity
- Process the webhook payloads in your application
Subscription Management
You can manage your webhook subscriptions through the Shelfless API. Available operations include:
- Create new subscriptions
- Update existing subscriptions
- List active subscriptions
- Activate/deactivate subscriptions
- Delete subscriptions
A subscription can contain one or more events to be forwarded to one endpoint. Multiple subscriptions can be created, however each event can only be present in one of those subscription. If the receiver needs to duplicate the message to multiple endpoints thathas to be done by the receiver, Shelfless does not support that scenario. The possibility to create multiple subscriptions is intended for the scenario where the receiver needs to receive different events/messages on different endpoints.
Important: To modify a subscription, use the update endpoint rather than unsubscribing and resubscribing. Unsubscribing may result in missed messages.
Shelfless ensures reliable message delivery through an automated retry system:
| Feature | Description |
|---|---|
| Retry Mechanism | If a webhook request fails (non-2XX response), Shelfless will automatically retry with an exponential backoff pattern |
| Dead-Letter Queue | If all retries are exhausted, messages are stored in a dead-letter queue |
| Requeuing | Messages can be requeued for delivery at any time, with no limit on retry attempts |
| Delivery Confirmation | Successfully delivered messages (2XX response) are confirmed and cannot be replayed |
| Message Persistence | No messages are lost as long as a subscription is active, even if the receiving endpoint is unreachable for an extended period |
| Message Ordering | Messages will be processed as soon as possible, but order is not guaranteed. Use the timestamps in the messages instead of relying on delivery order |
| Subscription Status | If a subscription is deleted, no messages are generated. There is no possibility of generating messages for events that occurred during an inactive subscription period |
For detailed API documentation on managing webhook subscriptions, visit the Shelfless API Documentation.
These events/messages are currently available for subscription: The messages are kept lightweight as in most cases the receiver knows what was ordered etc. For details if needed it is possible to act on a received message and poll details from the Shelfless API.
These events are available for webhook subscription. The webhook messages are intentionally lightweight, containing only essential information. For detailed data, you can query the Shelfless API using the information provided in the webhook payload.
| Event Type | Description |
|---|---|
sales_order.status |
Reports status updates for sales orders |
purchase_order.status |
Reports status updates for purchase orders |
article_master.updated |
Reports changes to master data |
return_order.status |
Reports status updates for return orders |
stock.adjustment |
Reports non-operational stock level changes (e.g., inventory checks, damaged goods) |
stock.adjustment.v2 |
Same as stock.adjustment but supports multiple stock type changes per event |
Event Envelope
All webhook events share the same outer envelope. The event-specific payload is nested inside data.event. Fields marked When available are omitted entirely when they contain no data — they will not appear as null. Not all fields will be present in every event.
| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier for the event. Use this to prevent duplicate processing. |
version |
string | API version identifier. Currently always “1”. |
timestamp |
number | Unix timestamp when the event was sent. |
type |
string | Event type identifier (e.g., “sales_order.status”). |
data.event |
object | Contains the event-specific payload. Fields vary by event type; see below. |
sales_order.status
Sent whenever a sales order changes status — for example when it is picked, packed, or shipped. Use order_number to correlate with your own order, and status_code / status_text to determine the new state. Tracking information is included when the order has been handed to a carrier.
| Field | Type | Presence | Notes |
|---|---|---|---|
order_number |
string | Always | |
warehouse_id |
string | When available | |
warehouse_name |
string | When available | |
status_code |
number | When available | Integer status code |
status_text |
string | When available | Human-readable status label |
updated_at |
number | When available | Unix timestamp of the status change |
items |
array | When available | Per-article fulfillment detail |
shipments |
array | When available | Present once a shipment has been created |
serviceCode |
string | When available | Carrier service code |
consignment |
array | When available | Consignment and package level detail |
ignoreSLA |
boolean | When available | When true, this status falls outside the SLA agreement |
{
"id": "45cf7e84-842a-4e3e-b3cf-cc466ddcd953",
"version": "1",
"timestamp": 1732019759,
"type": "sales_order.status",
"data": {
"event": {
"order_number": "20240903-9",
"warehouse_id": "BERGER",
"warehouse_name": "Bring Bergen",
"status_code": 230,
"status_text": "Shipped",
"updated_at": 1732019758,
"items": [
{
"article_id": "SKU1",
"quantity": 1,
"line_reference": "1",
"line_id": "1",
"detailed_items": [
{
"batchNumber": "L0343-Y",
"expiryDate": "2021-12-08",
"quantity": 1
}
]
}
],
"shipments": [
{
"carrier": "BPN",
"shipmentNumber": "70707730149100260",
"tracking": [
{
"number": "70707730149100260",
"link": "https://tracking.bring.com/tracking/70707730149100260"
}
]
}
],
"serviceCode": "SERVICEPAKKE"
}
}
}
purchase_order.status
Sent when a purchase order changes status — for example when it is acknowledged, received, or fully put away. Use order_number to correlate with your purchase order. The status object contains the current status and a per-line breakdown of received quantities.
| Field | Type | Presence | Notes |
|---|---|---|---|
order_number |
string | Always | |
warehouse_id |
string | When available | |
warehouse_name |
string | When available | |
reference_order |
string | When available | Supplier or customer reference on the order |
status |
object | When available | Current status detail |
status.status |
string | When available | Status label |
status.status_code |
number | When available | Integer status code |
status.last_updated |
string | When available | ISO 8601 timestamp |
status.items |
array | When available | Per-line received quantity detail |
status.items[].sku |
string | When available | |
status.items[].quantity |
number | When available | |
status.items[].line_id |
string | When available | |
status.items[].remaining_inbound_quantity |
number | When available | Quantity not yet received |
{
"id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"version": "1",
"timestamp": 1732019759,
"type": "purchase_order.status",
"data": {
"event": {
"order_number": "PO-20241103-1",
"warehouse_id": "BERGER",
"warehouse_name": "Bring Bergen",
"reference_order": "SUPPLIER-REF-001",
"status": {
"status": "Received",
"status_code": 100,
"last_updated": "2024-11-19T14:22:39Z",
"items": [
{
"sku": "SKU1",
"quantity": 50,
"line_id": "1",
"remaining_inbound_quantity": 0
}
]
}
}
}
}
article_master.updated
Sent when article master data for one of your SKUs is created or updated. The update_type field indicates the origin of the change: 1 means the update came from the warehouse management system (WMS), 2 means it came from a customer (API) update. Use the sku field to look up the full updated article via the Master Data API if you need the new attribute values.
| Field | Type | Presence | Notes |
|---|---|---|---|
customer_number |
string | When available | |
sku |
string | When available | The updated article SKU |
update_type |
number | When available | 1 = WMS update, 2 = customer (API) update |
unix_timestamp |
number | When available | Unix timestamp of the change |
timestamp |
string | When available | ISO 8601 timestamp of the change |
{
"id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
"version": "1",
"timestamp": 1732019759,
"type": "article_master.updated",
"data": {
"event": {
"customer_number": "12345",
"sku": "SKU1",
"update_type": 2,
"unix_timestamp": 1732019758,
"timestamp": "2024-11-19T14:22:38Z"
}
}
}
return_order.status
Sent when a return order changes status. The status field is an integer code representing the internal state; the status_text field provides the corresponding external status label visible to end users. Only events with a mapped external status are forwarded — internal-only transitions are suppressed.
| Field | Type | Presence | Notes |
|---|---|---|---|
customer_number |
string | When available | |
return_order_id |
string | When available | |
timestamp |
number | When available | Unix timestamp of the status change |
warehouse_id |
string | When available | |
status |
number | When available | Internal status code |
status_text |
string | When available | External status label; see values below |
Possible status_text values:
status_text |
Meaning |
|---|---|
WAITING_FOR_CONFIRMATION |
Return acknowledged by Shelfless, pending arrival |
INSPECTED |
Items inspected |
COMPLETED |
Return fully processed |
BOOKED |
Return booked into stock |
{
"id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a",
"version": "1",
"timestamp": 1732019759,
"type": "return_order.status",
"data": {
"event": {
"customer_number": "12345",
"return_order_id": "RO-20241103-7",
"timestamp": 1732019758,
"warehouse_id": "BERGER",
"status": 5,
"status_text": "COMPLETED"
}
}
}
stock.adjustment
Sent when a non-operational stock level change occurs for a SKU — for example a physical inventory count correction or a damage write-off. A single event always affects one balance type (e.g. PHYSICAL or BLOCKED). Use adjustment_id for deduplication. For events that affect multiple balance types simultaneously, see stock.adjustment.v2.
| Field | Type | Presence | Notes |
|---|---|---|---|
customer_number |
string | Always | |
sku |
string | Always | |
warehouse_id |
string | Always | |
adjustment |
number | Always | Positive = stock increase, negative = decrease |
unit |
string | Always | Unit of measure (e.g. PCE) |
balance_type |
string | Always | Stock category affected (e.g. PHYSICAL, BLOCKED) |
reason |
string | Always | Human-readable reason for the adjustment |
adjustment_id |
string | Always | Use for deduplication |
reason_code |
string | When available | Short code for the adjustment reason |
reference |
string | When available | External reference |
batch_number |
string | When available | |
source_created_epoch |
number | When available | Unix timestamp from the originating system |
event_created |
number | When available | Unix timestamp when the event was created |
source |
string | When available | Originating system identifier |
{
"id": "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b",
"version": "1",
"timestamp": 1732019759,
"type": "stock.adjustment",
"data": {
"event": {
"customer_number": "12345",
"sku": "SKU1",
"warehouse_id": "BERGER",
"adjustment": -5.0,
"unit": "PCE",
"balance_type": "PHYSICAL",
"reason": "Damaged during storage",
"reason_code": "DMG",
"adjustment_id": "ADJ-20241119-42",
"source_created_epoch": 1732019700,
"event_created": 1732019758
}
}
}
stock.adjustment.v2
An extended version of stock.adjustment that supports multiple stock type changes in a single event. This is used when a single operation affects more than one balance type — for example a SCRAPPED action that simultaneously decreases PHYSICAL stock and increases BLOCKED stock. Each entry in stock_changes represents one balance type change.
| Field | Type | Presence | Notes |
|---|---|---|---|
customer_number |
string | Always | |
sku |
string | Always | |
warehouse_id |
string | Always | |
stock_changes |
array | Always | One entry per affected balance type |
stock_changes[].stock_type |
string | Always | Stock category (e.g. PHYSICAL, BLOCKED) |
stock_changes[].adjustment |
number | Always | Positive = increase, negative = decrease |
created_at |
number | Always | Unix timestamp |
created_at_readable |
string | Always | ISO 8601 timestamp |
adjustment_id |
string | When available | Use for deduplication |
unit |
string | When available | Unit of measure |
batch_number |
string | When available | |
expiry_date |
string | When available | |
reason_code |
string | When available | Short code for the adjustment reason |
comment |
string | When available | Free-text comment |
{
"id": "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c",
"version": "1",
"timestamp": 1732019759,
"type": "stock.adjustment.v2",
"data": {
"event": {
"customer_number": "12345",
"sku": "SKU1",
"warehouse_id": "BERGER",
"adjustment_id": "ADJ-20241119-43",
"unit": "PCE",
"reason_code": "SCRAPPED",
"comment": "Damaged during storage, moved to blocked",
"stock_changes": [
{
"stock_type": "PHYSICAL",
"adjustment": -5.0
},
{
"stock_type": "BLOCKED",
"adjustment": 5.0
}
],
"created_at": 1732019758,
"created_at_readable": "2024-11-19T14:22:38Z"
}
}
}
Security
To ensure the authenticity and integrity of webhook messages, each request includes a shelfless-signature header. The header follows this format:
shelfless-signature: t=1731940023,v1=7774b82d476eca1b902d3b47c27bc375d705cba7d8939a26a7e444693e2a3ef3
Where:
t: Timestamp when the message was sentv1: HMAC signature of the payload
- Extract the timestamp and signature from the header
- Construct the signed payload string:
{timestamp}.{raw_payload} - Calculate the HMAC-SHA256 of the signed payload using your webhook secret. Please note that the secret is a hexadecimal string. Code example in Go:
hmacKey, _ := hex.DecodeString("secretKey")
mac := hmac.New(sha256.New, hmacKey)
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, rawPayload)))
macSum := mac.Sum(nil)
calcValue := fmt.Sprintf("%x", macSum)
- Compare your calculated signature with the received signature
Best Practices
- Always store the webhook secret securely
- Implement signature verification for all webhook requests
- Respond quickly to webhook requests (under 15 seconds) and process async if needed
Support
For additional support or questions about the webhook service:
- Visit our Shelfless API Documentation
- Contact Shelfless developers via your onboarding channel in slack.