E-commerce API
Integrate an online store with SnowBee: read the product catalogue and inventory for the store's assortment, create sales orders and click & collect reservations, manage B2C customers, and receive events when products or inventory change.
This guide assumes you have read Get started: how to authenticate, the base URL per scope, and the conventions (IDs, decimals, errors, rate limits) that apply to every request.
What you need
- A Client ID and Client Secret for an API client with access to your eCom store
- Your Tenant ID
- Your eCom Store ID — the
resourcein the token request
Request the token with scope: "ecom" and resource: "<eCom Store ID>". All endpoints on this page
live under:
https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/Everything you can read is scoped to the eCom store's assortment: GET /products lists only
products with at least one SKU on the assortment, and GET /products/:productId answers 404 for a
product that exists in SnowBee but is not on it.
API Endpoints Overview
Product Catalog
| Endpoint | Description |
|---|---|
GET /products | List all product IDs in the ecom store assortment |
GET /products/:productId | Get product details with SKUs, images, and prices |
GET /skus/:skuId/inventory | Get inventory availability by location |
GET /campaigns | List active and planned campaigns with SKU pricing |
POST /products/trigger_events | Trigger ProductSaved events for initial sync |
POST /inventory/trigger_events | Trigger InventoryOnHandChanged events for initial sync |
Product Schema (Reference Data)
| Endpoint | Description |
|---|---|
GET /product_schema/brands | List all brands |
GET /product_schema/product_categories | List product categories |
GET /product_schema/main_product_groups | List main product groups |
GET /product_schema/sub_product_groups | List sub product groups |
GET /product_schema/product_concepts | List product concepts |
GET /product_schema/exclusivity_levels | List exclusivity levels |
GET /product_schema/main_activities | List main activities |
GET /product_schema/product_colors | List colors with color codes |
GET /product_schema/sizes | List sizes with sort order |
GET /product_schema/dynamic_attributes | List custom attributes |
GET /product_schema/dynamic_attributes_options | List options for dynamic attributes |
GET /product_schema/unit_of_measures | List units of measure |
Orders
| Endpoint | Description |
|---|---|
POST /orders | Create a new sales order |
GET /orders/:orderIdOrNumber | Get order details by ID or order number, including invoices and return invoices |
POST /orders/:orderId/invoices/:invoiceId/refund | Confirm or report failure of a refund for a return invoice |
Customers
| Endpoint | Description |
|---|---|
PUT /customers/:customerId | Create or update customer (idempotent) |
GET /customers/:customerId | Get customer details |
PUT /customers/:customerId/addresses | Update customer delivery and invoice addresses |
GET /customers/:customerId/orders | Get customer order history |
PUT /push-customer | Create/update customer by phone number |
Click & Collect
| Endpoint | Description |
|---|---|
POST /click_and_collect | Create a click & collect reservation |
GET /retail_stores | List retail stores for click & collect |
Example: Create an Order
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/orders" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "019c1234-5678-7abc-8def-123456789012",
"deliveryAddress": {
"name": "John Doe",
"street": "Main Street 1",
"postCode": "0123",
"city": "Oslo",
"countryIsoCode": "NO",
"email": "john@example.com",
"phone": "+4712345678"
},
"invoiceAddress": {
"street": "Main Street 1",
"postCode": "0123",
"city": "Oslo",
"countryIsoCode": "NO",
"email": "john@example.com"
},
"lineItems": [
{
"skuId": "019c1234-5678-7abc-8def-123456789013",
"quantity": 2,
"netUnitPrice": 199.00,
"vatRate": 25.00
}
],
"freight": {
"freightAmount": 49.00,
"shippingMethodName": "Standard Shipping",
"providerData": "{\"trackingId\": \"ABC123\"}"
}
}'Line Item Pricing
netUnitPrice must be the price the customer paid for one unit. Price each line the way the
cash register does: apply the campaign price, apply any per-item markdown, and send the result. Do
not send a list price and expect SnowBee to work out what was charged.
// Required: the price the customer paid
{ "skuId": "...", "quantity": 1, "netUnitPrice": 239.60, "vatRate": 25.00 }netUnitOrderDiscountAmount is only for a reduction on top of the line's own price — a gift
voucher or a coupon spread across the order's lines. Never use it to express a campaign price or any
other markdown that belongs in netUnitPrice.
// Correct: campaign price in netUnitPrice, a 100,00 voucher share on top
{ "skuId": "...", "quantity": 1, "netUnitPrice": 239.60, "vatRate": 25.00,
"netUnitOrderDiscountAmount": 100.00, "campaignCode": "OUTLET1-26" }
// Wrong: list price with the campaign markdown as a discount
{ "skuId": "...", "quantity": 1, "netUnitPrice": 479.20, "vatRate": 25.00,
"netUnitOrderDiscountAmount": 239.60 }The second is wrong even when it happens to total the right amount. The two fields answer two different questions — what the item cost, and what was taken off it — and once a campaign markdown and a voucher are added together into one figure, SnowBee cannot split them apart again. An order sent that way cannot show the customer their own price breakdown, campaign reporting misses the sale, and the discount appears with nothing to explain where it came from.
The older shape is still accepted so existing integrations keep working. It is not the contract for new work, and orders sent that way lose the information above permanently — SnowBee never reprices an order that has been settled with the customer.
Recording the Campaign a Line Was Sold Under
Send campaignCode on every line sold under a campaign. Use the code from GET /campaigns,
together with that SKU's campaignPrice as the line's netUnitPrice:
{
"skuId": "019c1234-5678-7abc-8def-123456789013",
"quantity": 1,
"netUnitPrice": 239.60,
"vatRate": 25.00,
"campaignCode": "SUMMER24"
}Sent that way, the line is stored exactly as a campaign sale at the cash register: the sale counts towards the campaign, and the campaign is the line's price source.
| What you send | What SnowBee records |
|---|---|
campaignCode, and netUnitPrice equal to the campaign's campaignPrice for that SKU | Correct. The sale counts towards the campaign and the campaign is the line's price source |
campaignCode, and any other netUnitPrice | Rejected with 400. Put the campaign price in netUnitPrice and any further reduction, such as a voucher, in netUnitOrderDiscountAmount |
No campaignCode on a campaign sale | Incorrect. SnowBee falls back to matching the price against campaigns covering the SKU, which only succeeds when the match is exact and unambiguous. Anything else is recorded as a plain sale |
A campaign sale with no campaign on its lines is invisible to everything that reports by campaign — picking filters, pending sales order and purchase planning reports — even when the campaign is the whole reason the sale happened. Nothing recovers it afterwards: SnowBee does not guess a campaign from a line's price.
The code must belong to a released campaign covering the SKU on the order date, and the line must be
priced at that campaign's campaignPrice, or the request is rejected with 400.
A THREE_FOR_TWO campaign is named the same way, and priced at the basket's answer. Such an
offer sets no unit price of its own — what a third unit is worth depends on the whole basket — so
apply the offer yourself and send each line's blended per-unit price as netUnitPrice, exactly as a
cash register does: three units at 100,00 with one free is netUnitPrice 66,67, not 100,00 with a
discount alongside. The price must not exceed the SKU's normal price (which is what campaignPrice
returns for such a campaign), and netUnitOrderDiscountAmount stays reserved for reductions beyond
the offer — a voucher, a coupon share. The offer's value lands on the lines it applies to, never
spread across the whole order: a discount that lands where it was earned keeps the order's own
figures right, and a line sent this way is stored exactly as a 3-for-2 sale at the cash register,
so the price breakdown and campaign reporting read the two alike.
Line Item SKU Identification
You can identify SKUs in line items using either skuId or skuNumber:
// Using SKU ID (UUID)
{ "skuId": "019c1234-5678-7abc-8def-123456789013", "quantity": 2, ... }
// Using SKU number (your product code)
{ "skuNumber": "PROD-BLK-M", "quantity": 2, ... }At least one of skuId or skuNumber must be provided for each line item.
Freight / Shipping Information
You can optionally include freight information with your order using the freight object:
{
"freight": {
"freightAmount": 49.00,
"shippingMethodName": "Standard Shipping",
"providerData": "{\"trackingId\": \"ABC123\", \"serviceCode\": \"EXPRESS\"}"
}
}| Field | Type | Description |
|---|---|---|
freightAmount | decimal, nullable | Freight/shipping cost amount |
shippingMethodName | string, nullable | Name of the shipping method selected by the customer |
providerData | string, nullable | Opaque provider-specific data (e.g. JSON from a freight provider like Profrakt). SnowBee stores this as-is without parsing it. |
All fields are optional. The freight object itself is also optional. When an order has freight information, it is returned in the GET /orders/:orderIdOrNumber response.
Order Constraints
- Order must contain at least one line item
- Each line item must have either
skuIdorskuNumber - All SKUs must be in the eCom store's assortment
customerIdis optional - if omitted, the store's default customer is used- Delivery date is calculated as: order date + store's standard lead time
- Prices are in the company's default currency
campaignCodeis optional, and must name a released campaign covering that SKU on the order date
Example: Click & Collect Reservation
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/click_and_collect" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"retailStoreId": "019c1234-5678-7abc-8def-123456789012",
"reservationName": "John Doe",
"customerId": null,
"lineItems": [
{
"skuId": "019c1234-5678-7abc-8def-123456789013",
"quantity": 1
}
]
}'Response:
{
"parkedCartId": "019c1234-5678-7abc-8def-123456789014",
"cartId": "019c1234-5678-7abc-8def-123456789015",
"reservationName": "John Doe",
"retailStoreId": "019c1234-5678-7abc-8def-123456789012",
"retailStoreName": "Oslo Store",
"lineItems": [
{
"cartLineId": "019c1234-5678-7abc-8def-123456789016",
"skuId": "019c1234-5678-7abc-8def-123456789013",
"skuNumber": "SKU-001",
"productName": "Blue T-Shirt",
"quantity": 1,
"reservedFromWarehouse": "Oslo Store Warehouse"
}
],
"createdAt": "2025-01-20T10:30:00.000Z"
}Click & Collect Constraints
- Retail store must be in the eCom store's click & collect network
- Validates inventory availability before creating reservation
- Returns 400 if insufficient inventory
Example: Get Campaigns
curl -X GET "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/campaigns" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response:
{
"campaigns": [
{
"id": "019c1234-5678-7abc-8def-123456789012",
"code": "SUMMER24",
"name": "Summer Sale 2024",
"validFrom": "2024-06-01",
"validTo": "2024-08-31",
"currencyIsoCode": "NOK",
"status": "ACTIVE",
"campaignType": "NORMAL",
"skus": [
{
"skuId": "019c1234-5678-7abc-8def-123456789013",
"campaignPrice": 299.00,
"loyaltyProgramMembershipRequired": false
}
],
"loyaltyProgram": {
"id": "019c1234-5678-7abc-8def-456789abcdef",
"name": "Premium Members Club"
}
}
]
}Campaign statuses:
PLANNED- Campaign is scheduled but not yet activeACTIVE- Campaign is currently running
Campaign types:
NORMAL- Each SKU is sold at itscampaignPricefor as long as the campaign runsTHREE_FOR_TWO- Buy three of the campaign's SKUs and pay for two. The unit price is unchanged, socampaignPriceis the SKU's normal price and the discount is the cheapest of every third unit in the basket
The campaignType is set on the campaign and applies to all of its SKUs — it decides how to read each SKU's campaignPrice.
Errors specific to this API
The error envelope and the generic codes (UNAUTHORIZED, FORBIDDEN, BAD_REQUEST,
TOO_MANY_REQUESTS) are described in Get started. Codes you will see
from the e-commerce endpoints:
| Error Code | Description |
|---|---|
INVALID_REQUEST | Request validation failed (missing fields, invalid format) |
BAD_REQUEST | Request body could not be parsed (invalid JSON, wrong types) |
SKU_NOT_FOUND | SKU not found in store assortment. Message includes the SKU ID or number. |
CUSTOMER_NOT_FOUND | Customer with the specified ID does not exist |
UNAUTHORIZED | Missing or invalid authentication token |
FORBIDDEN | Token valid but lacks permission for this resource |
Important Notes
- All IDs are UUIDv7 format
- Timestamps are ISO 8601 format in UTC
- Phone numbers should be in E.164 format (e.g.,
+4712345678) - List endpoints return all items (no pagination)
- Only products/SKUs in your eCom store's assortment are accessible
Go-live checklist
Before pointing a production store at the API:
- Token handling: request a new token before the current one expires (1 hour); never embed the client secret in a browser or app
- Event notifications configured for the eCom store, and
POST /products/trigger_eventsandPOST /inventory/trigger_eventsrun once for the initial sync — do not pollGET /productson a schedule - Every order line sends the price the customer paid in
netUnitPrice, withcampaignCodeon campaign sales and only genuine on-top reductions innetUnitOrderDiscountAmount(see Line Item Pricing above) externalOrderIdis stable per order so a retriedPOST /ordersgets a409instead of a duplicate- Freight, delivery and invoice addresses complete; phone numbers in E.164
429responses honourRetry-Afterwith exponential backoff
Next steps
- Events — Azure Service Bus notifications for product, inventory and order changes
- API Reference — every endpoint and field, tag E-commerce