Event Notifications
SnowBee can send real-time notifications when important events occur, such as inventory changes or product updates. This allows your e-commerce platform to stay synchronized with SnowBee without polling the API.
Two transports deliver the same events, and a store can use either or both: an Azure Service Bus queue or topic, and webhooks posted to an endpoint you host. Whichever you choose, the subjects, the message body and the recommended actions below are identical.
Events are configured per eCom store and belong to the E-commerce integration; other scopes do not emit events today.
Configuration
Event notifications are configured per eCom Store in the SnowBee application.
Azure Service Bus
- Endpoint - Your Azure Service Bus namespace (e.g.,
your-namespace.servicebus.windows.net) - Entity Path - The queue or topic name to send messages to
- Shared Access Key Name - The name of your SAS policy (e.g.,
send) - Shared Access Key - The SAS key for authentication
Webhooks
- URL - The
https://endpoint each notification is posted to - Signing secret - A shared secret used to sign deliveries. SnowBee stores it encrypted and never returns it, so keep your own copy. You can replace it at any time; the previous secret stops verifying as soon as you do.
Each delivery is a POST with the message body below and these headers:
| Header | Value |
|---|---|
Snowbee-Subject | The event type, the same value the Service Bus subject property carries |
Snowbee-Event-Id | The event's unique id, also present in the body |
Snowbee-Signature | t=<unix seconds>,v1=<hex> |
To verify a delivery, compute HMAC-SHA256(signing secret, "<t>.<raw request body>") and compare it
to v1 using a constant-time comparison. Reject deliveries whose t is far from your own clock, so
a captured request cannot be replayed later.
Choosing which events you receive
By default a subscription receives every event below. In the eCom store's settings you can narrow it to the ones you actually handle, per transport: a store can take orders on its Service Bus queue and only product changes on its webhook.
Narrowing is an allowlist. Once you have chosen, an event type added to SnowBee later is not sent until you tick it, so a new subject never starts arriving at an endpoint that was not expecting it. A subscription that has never been narrowed keeps receiving everything, including anything added later.
Respond with any 2xx status once you have stored the event. SnowBee treats every other status, and
a timeout, as a failed delivery and retries with a growing delay before giving up. Retries mean the
same event id can arrive more than once, so make your handler idempotent.
Message Format
All event notifications are sent as JSON messages to your Azure Service Bus queue or topic. Each
message contains a subject property that indicates the event type, and a message body
with event details.
Message Structure
{
"entityId": "019c1234-5678-7abc-8def-123456789012",
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z",
"metadata": { "refundId": "019c9876-5432-7abc-8def-abcdef123456" }
}| Field | Type | Description |
|---|---|---|
entityId | string or null | The ID of the affected entity (product, SKU, order, etc.). May be null for schema-level events. |
eventId | string | A unique UUIDv7 identifier for this event. Use for idempotency and deduplication. |
datetime | string | ISO 8601 timestamp when the event occurred |
metadata | object or null | Optional key-value map with event-specific context. Present on some event types (see individual events below). |
The message subject property (accessible via ServiceBusReceivedMessage.Subject in C# or similar in other languages) indicates the event type.
Event Types
Products & Inventory
ProductUpdated
Triggered when a product is created, updated, deleted, or removed from the store's assortment. This includes changes to product attributes, SKUs, images, or prices.
Recommended action: Call
GET /products/:productIdto fetch the updated product details. If it returns404, remove or deactivate the product in your store: it has been deleted or is no longer in your assortment. If the product still exists butpublishedisfalse, keep it unpublished.
Subject: ProductUpdated
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Product ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z"
}
InventoryUpdated
Triggered when inventory availability changes for a SKU. This can happen due to:
- Stock received or adjusted in the warehouse
- Inventory reserved or released for orders
- Stock transfers between locations
The event is only sent if the SKU is in your eCom store's assortment.
Recommended action: Call
GET /skus/:skuId/inventoryto fetch the updated inventory levels.
Subject: InventoryUpdated
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // SKU ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z",
"metadata": {
"productId": "019c1111-2222-7abc-8def-333344445555",
"affectsEcomStock": "true",
"affectsClickAndCollect": "false"
}
}
The metadata says what the changed warehouse is to your store, so you can decide whether to act without calling back. The same warehouse is one store's own stock and another's pickup point, which is why this is resolved per store rather than left for you to work out. The warehouse itself is not named: warehouse IDs appear nowhere else in this API, so there would be nothing to match one against.
| Field | Meaning |
|---|---|
productId | The SKU's product, so you do not need your own SKU-to-product mapping |
affectsEcomStock | The warehouse fulfils this store's own orders, so what your web shop can sell may have changed |
affectsClickAndCollect | The warehouse is a pickup location for this store, so what can be collected there may have changed |
Both can be true when a store collects from its own warehouse. If you only show web availability,
ignore events where affectsEcomStock is false -- for a merchant with many shops those are the
large majority, since every sale on a shop floor produces one.
These events are debounced, so rapid changes to the same SKU arrive merged. Changes of different
kinds are never merged into each other: a pickup-only change cannot mask an eCom stock change that
follows it. The flags describe the window, so affectsEcomStock means at least one such warehouse
moved during it.
CampaignUpdated
Triggered when a campaign is created or updated. This includes changes to campaign dates, pricing, or SKU assignments.
Recommended action: Call
GET /campaignsto fetch the updated campaign list.
Subject: CampaignUpdated
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Campaign ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z"
}
Orders
OrderCreated
Triggered when a new order is created on your eCom store. This allows you to track orders as they enter SnowBee's fulfillment pipeline.
Recommended action: Call
GET /orders/:orderIdOrNumberto fetch the order details.
Subject: OrderCreated
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Order ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z"
}
OrderReadyForPaymentCapture
Triggered when a capture is created for an order on an eCom store that has payment capture enabled. This indicates that the order (or part of it) has been picked and is awaiting payment capture before shipment can proceed. An order may have multiple captures (e.g., partial fulfillment), each with its own independent capture lifecycle.
Shipment of each capture is blocked until its payment capture is confirmed. Call GET /orders/:orderIdOrNumber to fetch the order with the captures array (which includes each capture's id, status, and lines), then call POST /orders/:orderId/captures/:captureId/payment_capture with status CONFIRMED or FAILED.
Required action: Call
GET /orders/:orderIdOrNumberto get capture details and line items, capture payment externally, then callPOST /orders/:orderId/captures/:captureId/payment_capturewith the capture result.
Subject: OrderReadyForPaymentCapture
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Order ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z",
"metadata": { "captureId": "019c9876-5432-7abc-8def-abcdef123456" }
}
PickedSalesOrderLinePriceChanged
Triggered when the net unit sales price or the discount of a picked order line is edited in SnowBee after its picking list was closed, but before those lines have shipped. The capture amount was worked out when the picking list closed, so an edit in this window leaves the amount you were asked to capture out of date. One event is sent per picking list, listing the lines that were edited.
Required action: Call
GET /orders/:orderIdOrNumberto re-read the affected lines and the capture total before confirming the payment capture.
Subject: PickedSalesOrderLinePriceChanged
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Order ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z",
"metadata": {
"captureId": "019c9876-5432-7abc-8def-abcdef123456",
"salesOrderLineIds": "019c1111-...,019c2222-..." // Comma-separated
}
}
OrderFulfilled
Triggered when a receipt is created for an order, indicating the order (or part of it) has been fulfilled and is ready for shipping.
Recommended action: Call
GET /orders/:orderIdOrNumberto fetch the order with receipt details.
Subject: OrderFulfilled
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Order ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z"
}
OrderReadyForRefund
Triggered when a refund is created for an order. This indicates that a customer return has been processed in SnowBee and the e-commerce platform should issue a refund.
Call GET /orders/:orderIdOrNumber to fetch the order — the refunds array will contain the refund(s) with netAmount and grossAmount showing what should be refunded. After processing the refund externally, call POST /orders/:orderId/refunds/:refundId/confirm with status CONFIRMED to close the refund, or FAILED if the refund could not be processed.
Required action: Call
GET /orders/:orderIdOrNumberto get refund details, process the refund via your payment provider, then callPOST /orders/:orderId/refunds/:refundId/confirmwith the result.
Subject: OrderReadyForRefund
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Order ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z",
"metadata": { "refundId": "019c9876-5432-7abc-8def-abcdef123456" }
}
OrderShipped
Triggered when a shipment is created for an order, indicating the order (or part of it) has been shipped.
Recommended action: Call
GET /orders/:orderIdOrNumberto fetch the order with shipment details.
Subject: OrderShipped
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Order ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z"
}
Stores
RetailStoreUpdated
Triggered when a retail store's name, store number, status, or visit address changes. This includes a store being released, so use this event to discover new click & collect stores. This event is only sent for retail stores that belong to a company linked to your eCom store via click & collect.
Recommended action: Call
GET /retail_storesto fetch the updated store details.
Subject: RetailStoreUpdated
{
"entityId": "019c1234-5678-7abc-8def-123456789012", // Retail Store ID
"eventId": "019c5678-1234-7abc-8def-987654321098",
"datetime": "2025-01-20T10:30:00.000Z"
}
Product Schema
These events are triggered when product schema data (lookup tables) are updated. The entityId will be null for these events since they affect the entire schema.
| Subject | Description | API Endpoint |
|---|---|---|
BrandsUpdated | A brand was added, modified, or removed | /product_schema/brands |
ProductCategoriesUpdated | A product category was added, modified, or removed | /product_schema/product_categories |
MainProductGroupsUpdated | A main product group was added, modified, or removed | /product_schema/main_product_groups |
SubProductGroupsUpdated | A sub product group was added, modified, or removed | /product_schema/sub_product_groups |
ProductConceptsUpdated | A product concept was added, modified, or removed | /product_schema/product_concepts |
ExclusivityLevelsUpdated | An exclusivity level was added, modified, or removed | /product_schema/exclusivity_levels |
ProductColorsUpdated | A product color was added, modified, or removed | /product_schema/product_colors |
SizesUpdated | A size was added, modified, or removed | /product_schema/sizes |
DynamicAttributesUpdated | A dynamic attribute was added, modified, or removed | /product_schema/dynamic_attributes |
DynamicAttributeOptionsUpdated | A dynamic attribute option was added, modified, or removed | /product_schema/dynamic_attributes_options |
Best Practices
Idempotency
Each event includes a unique eventId (UUIDv7). Store processed event IDs to avoid
reprocessing the same event if it's delivered multiple times (at-least-once delivery).
Event Ordering
Events may arrive out of order. Use the datetime field to determine the chronological
order of events. For inventory updates, always fetch the latest state from the API rather than
applying incremental changes.
Handling High Volume
During bulk imports or updates, you may receive many events in a short period. Consider:
- Implementing a delay/debounce to coalesce multiple events for the same entity
- Using Azure Service Bus sessions or partitioning for ordered processing per entity
Error Handling
If your message handler fails, the message will be returned to the queue for retry (depending on your Azure Service Bus configuration). Implement proper dead-letter queue handling for messages that consistently fail.
Example: C# Message Handler
Reference: POST /orders/{salesOrderId}/captures/{captureId}/payment_capture, POST /orders/{salesOrderId}/refunds/{refundId}/confirm.
using Azure.Messaging.ServiceBus;
using System.Text.Json;
public class SnowBeeEventHandler
{
private readonly SnowBeeApiClient _apiClient;
public async Task ProcessMessageAsync(ServiceBusReceivedMessage message)
{
var subject = message.Subject;
var body = JsonSerializer.Deserialize<SnowBeeEvent>(message.Body);
switch (subject)
{
case "InventoryUpdated":
await HandleInventoryUpdated(body.EntityId);
break;
case "ProductUpdated":
await HandleProductUpdated(body.EntityId);
break;
case "CampaignUpdated":
await HandleCampaignUpdated();
break;
case "OrderCreated":
await HandleOrderCreated(body.EntityId);
break;
case "OrderReadyForPaymentCapture":
await HandleOrderReadyForPaymentCapture(body);
break;
case "OrderReadyForRefund":
await HandleOrderReadyForRefund(body);
break;
case "OrderFulfilled":
await HandleOrderFulfilled(body.EntityId);
break;
case "OrderShipped":
await HandleOrderShipped(body.EntityId);
break;
case "RetailStoreUpdated":
await HandleRetailStoreUpdated(body.EntityId);
break;
case "BrandsUpdated":
case "ProductCategoriesUpdated":
case "SizesUpdated":
// ... other schema events
await HandleSchemaUpdated(subject);
break;
}
}
private async Task HandleInventoryUpdated(string skuId)
{
var inventory = await _apiClient.GetSkuInventory(skuId);
// Update your local inventory cache/database
}
private async Task HandleProductUpdated(string productId)
{
var product = await _apiClient.GetProduct(productId);
// Update your local product catalog
}
private async Task HandleCampaignUpdated()
{
var campaigns = await _apiClient.GetCampaigns();
// Update your local campaign/pricing data
}
private async Task HandleOrderCreated(string orderId)
{
var order = await _apiClient.GetOrder(orderId);
// Track new order, update local order database
}
private async Task HandleOrderReadyForPaymentCapture(SnowBeeEvent body)
{
var orderId = body.EntityId;
var captureId = body.Metadata?["captureId"];
var order = await _apiClient.GetOrder(orderId);
// Find the capture from the event metadata, or fall back to scanning for PENDING
var capture = order.Captures.FirstOrDefault(c => c.Id == captureId)
?? order.Captures.FirstOrDefault(c => c.Status == "PENDING");
if (capture != null)
{
// Calculate capture amount from capture lines and order line items
// Capture payment via your payment provider
// Then report capture result to SnowBee:
// POST /orders/{orderId}/captures/{captureId}/payment_capture
// { "status": "CONFIRMED", "capturedAmount": "1499.00" }
// or { "status": "FAILED" } if capture failed
await _apiClient.UpdatePaymentCapture(orderId, capture.Id, "CONFIRMED", capturedAmount);
}
}
private async Task HandleOrderReadyForRefund(SnowBeeEvent body)
{
var orderId = body.EntityId;
var refundId = body.Metadata?["refundId"];
var order = await _apiClient.GetOrder(orderId);
// Use the refundId from metadata to find the specific refund
var refund = order.Refunds.FirstOrDefault(r => r.Id == refundId && r.Status == "OPEN");
if (refund != null)
{
// Use the grossAmount on the refund for the refund amount
var refundAmount = refund.GrossAmount;
// Process refund via your payment provider
// Then confirm the refund to SnowBee:
// POST /orders/{orderId}/refunds/{refundId}/confirm
// { "status": "CONFIRMED", "refundedAmount": "299.00" }
await _apiClient.ConfirmRefund(orderId, refund.Id, "CONFIRMED", refundAmount);
}
}
private async Task HandleOrderFulfilled(string orderId)
{
var order = await _apiClient.GetOrder(orderId);
// Process fulfillment, update order status, trigger shipping
}
private async Task HandleOrderShipped(string orderId)
{
var order = await _apiClient.GetOrder(orderId);
// Update order tracking, notify customer
}
private async Task HandleRetailStoreUpdated(string retailStoreId)
{
var stores = await _apiClient.GetRetailStores();
// Update your local store data (name, number, address)
}
private async Task HandleSchemaUpdated(string schemaType)
{
// Refresh the relevant schema data
// e.g., reload brands, categories, sizes, etc.
}
}
public record SnowBeeEvent(
string? EntityId,
string EventId,
string Datetime,
Dictionary<string, string>? Metadata
);Triggering Initial Sync
When setting up a new integration, you can trigger events for all entities in your eCom store assortment to populate your local data without polling.
The call answers 202 Accepted at once and the events follow gradually, up to 300 a minute, so a
store of 10,000 SKUs takes about 40 minutes. Only your eCom store receives them. Calling again
while a resync is still queued or running adds nothing, so a retry after a timeout is safe.
Trigger Product Events
Reference: POST /products/trigger_events.
POST /v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/products/trigger_events
Authorization: Bearer YOUR_ACCESS_TOKEN
Response (202 Accepted):
{
"eventsTriggered": 1250,
"message": "Queued 1250 product events"
}
This sends ProductUpdated events for all products in your assortment.
Trigger Inventory Events
Reference: POST /inventory/trigger_events.
POST /v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/inventory/trigger_events
Authorization: Bearer YOUR_ACCESS_TOKEN
Response (202 Accepted):
{
"eventsTriggered": 3500,
"message": "Queued 3500 inventory events"
}
This sends InventoryUpdated events for all SKUs in your assortment, allowing
you to sync current inventory levels.
Related Links
- Getting Started - Authentication and first API calls
- API Reference - Full OpenAPI documentation
- Azure Service Bus Documentation (opens in a new tab)