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 resource in 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

EndpointDescription
GET /productsList all product IDs in the ecom store assortment
GET /products/:productIdGet product details with SKUs, images, and prices
GET /skus/:skuId/inventoryGet inventory availability by location
GET /campaignsList active and planned campaigns with SKU pricing
POST /products/trigger_eventsTrigger ProductSaved events for initial sync
POST /inventory/trigger_eventsTrigger InventoryOnHandChanged events for initial sync

Product Schema (Reference Data)

EndpointDescription
GET /product_schema/brandsList all brands
GET /product_schema/product_categoriesList product categories
GET /product_schema/main_product_groupsList main product groups
GET /product_schema/sub_product_groupsList sub product groups
GET /product_schema/product_conceptsList product concepts
GET /product_schema/exclusivity_levelsList exclusivity levels
GET /product_schema/main_activitiesList main activities
GET /product_schema/product_colorsList colors with color codes
GET /product_schema/sizesList sizes with sort order
GET /product_schema/dynamic_attributesList custom attributes
GET /product_schema/dynamic_attributes_optionsList options for dynamic attributes
GET /product_schema/unit_of_measuresList units of measure

Orders

EndpointDescription
POST /ordersCreate a new sales order
GET /orders/:orderIdOrNumberGet order details by ID or order number, including invoices and return invoices
POST /orders/:orderId/invoices/:invoiceId/refundConfirm or report failure of a refund for a return invoice

Customers

EndpointDescription
PUT /customers/:customerIdCreate or update customer (idempotent)
GET /customers/:customerIdGet customer details
PUT /customers/:customerId/addressesUpdate customer delivery and invoice addresses
GET /customers/:customerId/ordersGet customer order history
PUT /push-customerCreate/update customer by phone number

Click & Collect

EndpointDescription
POST /click_and_collectCreate a click & collect reservation
GET /retail_storesList 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 sendWhat SnowBee records
campaignCode, and netUnitPrice equal to the campaign's campaignPrice for that SKUCorrect. The sale counts towards the campaign and the campaign is the line's price source
campaignCode, and any other netUnitPriceRejected with 400. Put the campaign price in netUnitPrice and any further reduction, such as a voucher, in netUnitOrderDiscountAmount
No campaignCode on a campaign saleIncorrect. 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\"}"
  }
}
FieldTypeDescription
freightAmountdecimal, nullableFreight/shipping cost amount
shippingMethodNamestring, nullableName of the shipping method selected by the customer
providerDatastring, nullableOpaque 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 skuId or skuNumber
  • All SKUs must be in the eCom store's assortment
  • customerId is 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
  • campaignCode is 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 active
  • ACTIVE - Campaign is currently running

Campaign types:

  • NORMAL - Each SKU is sold at its campaignPrice for as long as the campaign runs
  • THREE_FOR_TWO - Buy three of the campaign's SKUs and pay for two. The unit price is unchanged, so campaignPrice is 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 CodeDescription
INVALID_REQUESTRequest validation failed (missing fields, invalid format)
BAD_REQUESTRequest body could not be parsed (invalid JSON, wrong types)
SKU_NOT_FOUNDSKU not found in store assortment. Message includes the SKU ID or number.
CUSTOMER_NOT_FOUNDCustomer with the specified ID does not exist
UNAUTHORIZEDMissing or invalid authentication token
FORBIDDENToken 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_events and POST /inventory/trigger_events run once for the initial sync — do not poll GET /products on a schedule
  • Every order line sends the price the customer paid in netUnitPrice, with campaignCode on campaign sales and only genuine on-top reductions in netUnitOrderDiscountAmount (see Line Item Pricing above)
  • externalOrderId is stable per order so a retried POST /orders gets a 409 instead of a duplicate
  • Freight, delivery and invoice addresses complete; phone numbers in E.164
  • 429 responses honour Retry-After with exponential backoff

Next steps

  • Events — Azure Service Bus notifications for product, inventory and order changes
  • API Reference — every endpoint and field, tag E-commerce