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 the required eCom permissions and your eCom stores selected
- Your Tenant ID
- Your eCom Store ID — used in each endpoint URL
Request a token with scope: "ecom:read" for reads, scope: "ecom:write" for mutations, or
scope: "ecom:read ecom:write" for both. Omit resource. One token can access every eCom store
selected on the client, with the same permissions for each store. All endpoints on this page live
under:
https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/Permissions
ecom:read covers all GET operations and the query operations
POST /carts/:cartId/delivery_options
and POST /carts/:cartId/pickup_points.
These POST operations look up delivery information without changing the cart.
ecom:write covers mutations, including creating and updating carts, checkout, sales-order
creation, customer and loyalty-enrollment updates, payment-capture and refund confirmations, and
triggering product or inventory events. Write permission does not grant reads; a full storefront
integration needs both scopes. Each request also checks that its eCom Store ID is included in the
token's selected stores. An unselected store or missing operation scope returns 403.
The legacy ecom token request remains supported for clients with both read and write grants; see
Get started.
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
Settings
| Endpoint | Description |
|---|---|
GET /settings | Read integration settings, including the tenant time zone |
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 | Queue ProductUpdated events for initial sync |
POST /inventory/trigger_events | Queue InventoryUpdated 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/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/refunds/:refundId/confirm | 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, including the store's guest orders delivered to the customer's email |
GET /customers/by_email/:email | Find a customer by email |
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 |
Catalog listing Experimental
| Endpoint | Description |
|---|---|
GET /products/search | List and search sellable products as summaries with names, image, prices and availability; filter, sort and page with a cursor |
Store Experimental
| Endpoint | Description |
|---|---|
GET /store | The store's name, currency, the loyalty program member prices refer to, and whether checkout takes payment |
Carts Experimental
| Endpoint | Description |
|---|---|
POST /carts | Create an empty cart for the store |
GET /carts/:cartId | Get a cart and its lines |
POST /carts/:cartId/lines | Add a SKU to the cart (merges into an existing line for the same SKU) |
PUT /carts/:cartId/lines/:lineId | Set a cart line's quantity |
DELETE /carts/:cartId/lines/:lineId | Remove a line from the cart |
PUT /carts/:cartId/customer | Bind the cart to a logged-in shopper's customer: priced on their membership, ordered in their name, merged with their other cart |
DELETE /carts/:cartId/customer | Make the cart anonymous again at logout |
POST /carts/:cartId/delivery_options | Delivery options for the cart to an address, priced live by the merchant's carrier |
POST /carts/:cartId/pickup_points | Pickup points near an address for a carrier product |
POST /carts/:cartId/payment_session | Open an Adyen Drop-in session for the cart plus the chosen freight |
POST /carts/:cartId/checkout | Turn the cart into an order, verifying the payment when the store takes one |
Building a web shop on these? Build a storefront walks the shopper's whole path.
A cart is deleted when it is checked out; the order is the record from then on. A cart nobody has
touched for 30 days is deleted as well, so a stored cart id can stop resolving: treat 404 on a
cart as "start a new cart", not as an error.
Example: Create an Order
Reference: POST /orders.
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 at the sale instant, 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 at the sale instant
Example: Click & Collect Reservation
A reservation parks one or more SKUs in a retail store for the shopper to collect and pay for over
the counter. Pick the store from GET /retail_stores; GET /skus/:skuId/inventory lists the same
stores with their available quantity, so a product page can show where a variant is in stock.
Reference: POST /click_and_collect.
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": "Ola Nordmann",
"reservationPhone": "+4712345678",
"reservationEmail": "ola@example.com",
"lineItems": [
{
"skuId": "019c1234-5678-7abc-8def-123456789013",
"quantity": "1"
}
]
}'Response (202 Accepted):
{
"id": "019c1234-5678-7abc-8def-123456789014"
}The id is the reservation's reference in SnowBee; the store sees it as a parked click & collect cart.
customerId is optional: without it, an existing B2C customer is matched by reservationEmail, then
reservationPhone, and otherwise the store's walk-in customer is used.
Click & Collect pricing
A reservation is paid in the collecting store, so that store prices it: its own current sales price and
campaigns, applied when the reservation arrives and again when the cash register opens it. A line may
carry the webshop's netUnitPrice (excluding VAT) and vatRate, but neither is a pricing input. Both
are optional; when omitted, the store's price and the product's VAT rate in the store's country are
used as the initial values. They are required only for a SKU the store has no sales price for, and
the request is rejected with 400 if they are missing then.
Click & Collect Constraints
- The retail store must be released and belong to the eCom store's click & collect company
- Each line needs
skuIdorskuNumber - Available inventory in the store is checked before the reservation is created;
400if insufficient
Example: List and Search Products Experimental
GET /products/search returns the store's sellable products -- published in the assortment, released
and carrying a current sales price -- one page at a time. Without a query the default sort is
newest; with one it is relevance. Every dimension filter is repeatable, so a category page is one
call:
Reference: GET /products/search.
curl "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/products/search?mainProductGroupId=GROUP_ID&sort=price_asc&limit=24" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"{
"currencyIsoCode": "NOK",
"items": [
{
"id": "019c1234-5678-7abc-8def-123456789012",
"productNumber": "100234",
"productName": { "value": { "NO": "Vinterjakke" } },
"brand": { "id": "019c1234-5678-7abc-8def-123456789040", "name": { "value": { "NO": "Norr" } } },
"productCategory": { "id": "019c1234-5678-7abc-8def-123456789041", "name": { "value": { "NO": "Klær" } } },
"mainProductGroup": { "id": "019c1234-5678-7abc-8def-123456789042", "name": { "value": { "NO": "Jakker" } } },
"subProductGroup": { "id": "019c1234-5678-7abc-8def-123456789043", "name": { "value": { "NO": "Vinterjakker" } } },
"mainImageUrl": "https://imagedelivery.net/ACCOUNT/IMAGE_ID/shopifyProductImage",
"price": { "price": "2999", "campaignPrice": "2499", "loyaltyMemberPrice": null },
"offers": [],
"inStock": true,
"newFrom": "2026-09-01",
"newTo": null
}
],
"nextCursor": "bmV3ZXN0...",
"previousCursor": null
}Parameters: query (free text over number, name, brand, groups, sku numbers and barcodes -- a comma
separates alternatives, a space requires every word), productId (repeatable UUIDs; a lookup rather
than a search, for re-reading products you already know), brandId, productCategoryId,
mainProductGroupId, subProductGroupId (all repeatable UUIDs), inStock (true lists only products
with a sku available for ecom in the store's warehouse), sort (relevance, newest, name,
price_asc, price_desc, optionally prefixed by in_stock to list available products first, as in
in_stock,name), limit (default 24, max 100) and cursor.
Pass nextCursor back as cursor for the next page and previousCursor for the page before; each is
null at the respective end of the listing. A cursor is only
valid for the filters and sort it was issued for -- anything else answers 400 INVALID_CURSOR.
Prices are gross, in the store's currency, and the campaign price is the one the cart will charge.
loyaltyMemberPrice is the members-only campaign price, for shoppers who are members of the store's
loyalty program (GET /store). offers lists the 3 for 2 campaigns covering the product, each with
campaignType, name and whether it is members-only; such an offer has no unit price and is applied by
the cart. Both fields are also on GET /products/:productId.
Names are localized strings keyed by language code, as everywhere else in the catalog API.
Dimension IDs for filters come from the product_schema endpoints above.
Example: Build a Cart Experimental
The cart stores what the shopper wants to buy and SnowBee prices it on every read -- the same sales prices, campaigns, 3 for 2 offers and VAT rules the cash register applies. Checking out turns the cart into an ordinary ecom order and deletes the cart. A store set up for web payment takes the payment through Adyen's Drop-in first.
A cart belongs to one ecom store and is reached only with that store's token, so a cart ID alone grants nothing. Keep the cart ID in the shopper's session (a cookie or your own storage) and never put it in a URL. Every cart call returns the complete cart, priced.
Create a cart:
Reference: POST /carts.
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/carts" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"{
"id": "019c1234-5678-7abc-8def-123456789020",
"progress": "ACTIVE",
"lines": [],
"pricing": { "currencyIsoCode": "NOK", "netAmount": "0", "vatAmount": "0", "grossAmount": "0", "complete": true }
}Add two of a SKU, then add one more of the same SKU -- the cart still has one line, now with quantity 3:
Reference: POST /carts/{cartId}/lines.
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/carts/CART_ID/lines" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "skuId": "019c1234-5678-7abc-8def-123456789013", "quantity": 2 }'{
"id": "019c1234-5678-7abc-8def-123456789020",
"progress": "ACTIVE",
"lines": [
{
"id": "019c1234-5678-7abc-8def-123456789021",
"skuId": "019c1234-5678-7abc-8def-123456789013",
"quantity": 2,
"pricing": {
"vatRate": "25",
"basicGrossUnitPrice": "299",
"basicNetUnitPrice": "239.2",
"netUnitPrice": "199.2",
"grossUnitPrice": "249",
"netLineAmount": "398.4",
"grossLineAmount": "498",
"campaign": {
"id": "019c1234-5678-7abc-8def-123456789030",
"code": "SUMMER24",
"name": "Summer sale",
"campaignType": "NORMAL",
"netUnitPrice": "199.2",
"applies": true
}
}
}
],
"pricing": { "currencyIsoCode": "NOK", "netAmount": "398.4", "vatAmount": "99.6", "grossAmount": "498", "complete": true }
}Every money value is a string, never a JSON number: amounts carry at most two decimals, net unit
prices up to four because that is how SnowBee stores them. basicGrossUnitPrice is the sku's ordinary price, grossUnitPrice
what the shopper pays -- show both for a strike-through. For a THREE_FOR_TWO campaign the unit price
is the blended price after the free unit is spread across the line; applies is false while the basket
has fewer than three qualifying units.
Set a line's quantity with PUT .../lines/LINE_ID and a body of { "quantity": 1 }; remove it
with DELETE .../lines/LINE_ID. Each line also carries sku -- sku number, barcode, product id, number
and localized name, color and size names, main image URL -- so a cart page needs no further calls.
Check out with the shopper's addresses and, when the carrier's checkout UI has priced it, the same
freight block POST /orders takes. The response is the order; the cart is gone afterwards, and
repeating the call returns the same order:
Reference: POST /carts/{cartId}/checkout.
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/carts/CART_ID/checkout" \\
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"deliveryAddress": { "name": "Kari Nordmann", "street": "Storgata 1", "postCode": "0155", "city": "Oslo", "countryIsoCode": "NO", "email": "kari@example.com", "phone": "+4790000000" },
"invoiceAddress": { "name": "Kari Nordmann", "street": "Storgata 1", "postCode": "0155", "city": "Oslo", "countryIsoCode": "NO", "email": "kari@example.com" }
}'{ "orderId": "019c1234-5678-7abc-8def-123456789050", "orderNumber": "100042" }Cart Constraints
- Only carts with
progress: "ACTIVE"can be changed; anything else answers409 CONFLICT - Quantities must be greater than zero -- delete the line instead of setting
0(422 VALIDATION_ERROR) - A SKU must be published in the store's assortment and have a current sales price in the store's
currency and a VAT rate for the store's country to be added (
422 VALIDATION_ERROR) - Pricing is resolved for B2C stores; a B2B store's cart returns
pricing: null - A line whose price disappears after it was added keeps
pricing: nulland the cart reportspricing.complete: false - A cart never holds two lines for the same SKU; adding a SKU that is already in the cart increases that line
- Unknown cart or line IDs answer
404 NOT_FOUND; malformed IDs answer400 INVALID_UUID - Checkout needs a non-empty, fully priced cart in a B2C store (
422 VALIDATION_ERRORotherwise) and, iffreightis given, its amount, VAT rate and all three carrier codes - The cart id becomes the order's
externalOrderId(and its external order number unless you sendexternalOrderNumber); show the shopper the returnedorderNumber - A cart bound to a customer (
PUT /carts/:cartId/customer) is priced on that customer's membership of the store's loyalty program and checked out as their order, whatevercustomerIdthe checkout sends; binding answers404for an unknown customer and409for a cart bound to someone else
Delivery options Experimental
Freight is priced by the merchant's carrier, through Profrakt, on the merchant's own agreements. SnowBee stores no shipping options and no prices: it asks Profrakt what the cart costs to deliver to the shopper's address and hands the answer on.
Reference: POST /carts/{cartId}/delivery_options.
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/carts/CART_ID/delivery_options" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "address": { "street": "Storgata 1", "postCode": "0155", "city": "Oslo", "countryIsoCode": "NO" } }'{
"options": [
{
"carrierCode": "bring2",
"carrierName": "Bring",
"carrierProductCode": "bring2_parcel_pickup_point",
"carrierProductName": "Pakke til hentested",
"carrierAgreementCode": "2728",
"netFreightAmount": "79.00",
"freightVatRate": "25",
"grossFreightAmount": "98.75",
"expectedDelivery": "12.05.2026",
"workingDays": 2,
"pickupPointRequired": true
}
]
}Options are sorted by gross freight. For an option with pickupPointRequired, list the pickup points
near the address with POST /carts/:cartId/pickup_points (body: carrierCode, carrierProductCode,
address) and let the shopper choose one. The chosen option's carrier fields, netFreightAmount and
freightVatRate are the checkout's freight block, with the pickup point as
carrierProductPayload: { "partnerNumber": "...", "partnerName": "..." }. Send the same freight
block when opening the payment session, so the shopper authorises goods plus freight. Options answer
409 when the store's company has no Profrakt transport configuration.
Paying for the cart Experimental
A store set up for web payment takes the shopper's money before the order exists, and checkout refuses a cart that has not been paid. The credentials stay in SnowBee; the browser gets only what Adyen's Drop-in needs.
Reference: POST /carts/{cartId}/payment_session.
curl -X POST "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/carts/CART_ID/payment_session" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "returnUrl": "https://shop.example/checkout/return", "shopperEmail": "ola@example.com", "freight": { ... } }'{
"paymentId": "019c1234-5678-7abc-8def-123456789020",
"sessionId": "CS451F2AB1...",
"sessionData": "Ab02b4c0!...",
"clientKey": "test_ABC...",
"environment": "test",
"amount": "1198.00",
"currencyIsoCode": "NOK"
}Mount the Drop-in with sessionId, sessionData, clientKey and environment. When it reports the
session complete, check out with the paymentId and the Drop-in's sessionResult:
{ "deliveryAddress": { ... }, "invoiceAddress": { ... }, "payment": { "paymentId": "019c1234-...-789020", "sessionResult": "X3XtfGC7..." } }SnowBee verifies the session with Adyen and that the authorised amount is still what the cart costs
including the freight sent with the checkout.
A cart whose total changed after the session was opened answers 422; open a new session and pay
again. A refused or expired session answers 422 as well. The authorisation is captured by SnowBee
when the order is picked; nothing else is required of the storefront. A store without web payment
configured answers 409 to the session call and 422 to a checkout that carries payment.
Example: Get Settings
Reference: GET /settings.
curl -X GET "https://api.snowbee.no/v1/tenants/TENANT_ID/ecom_stores/ECOM_STORE_ID/settings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response:
{
"timeZone": "Europe/Oslo"
}timeZone is the tenant's IANA time zone. It is configured when the tenant is created;
changing the tenant time zone is not supported. This endpoint is read-only and uses the
same e-commerce token and store permissions as the other endpoints. It is available even
when the store has no campaigns.
Campaign boundaries and other tenant-local dates and times use this zone. Its UTC offset can vary with daylight saving; use the offset supplied on each resolved timestamp rather than assuming one fixed offset for the tenant.
Example: Get Campaigns
Reference: 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",
"validFromTime": "00:00",
"validToTime": "24:00",
"timeZone": "Europe/Oslo",
"validFromAt": "2024-06-01T00:00:00+02:00",
"validToAt": "2024-09-01T00:00:00+02:00",
"memberLevels": [
{ "type": "NORMAL", "id": null, "code": null, "name": null },
{ "type": "CONFIGURED", "id": "019c1234-5678-7abc-8def-123456789014", "code": "GOLD", "name": "Gold" }
],
"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"
}
}
]
}validFrom and validTo remain date-only fields (YYYY-MM-DD) for compatibility.
The new validFromAt and validToAt fields are ISO 8601 datetimes with UTC offsets,
resolved from the selected dates and times using the tenant's time zone. That zone
is included as timeZone and is also available from GET /settings; changing it is not supported.
The campaign has one continuous validity window. validFromAt is inclusive and
validToAt is exclusive: apply the campaign when validFromAt <= now < validToAt.
Use these resolved datetimes for activation and expiry, rather than comparing the dates alone.
validFromTime and validToTime use HH:mm; 24:00 means midnight on the next calendar date.
The default 00:00 start and 24:00 end preserve full-day campaigns. For example, 4 October
at 00:00 through 6 October at 24:00 ends exactly at midnight starting 7 October. An end
of 21:00 makes the campaign inactive at 21:00 on its selected end date.
memberLevels lists the campaign's selected loyalty-program levels. CONFIGURED entries
include the SnowBee level ID, provider code and display name. NORMAL explicitly selects
program members with no configured provider level; it does not select non-members. An empty
list adds no restriction on member level. The campaign's loyaltyProgram and each SKU's
loyaltyProgramMembershipRequired still describe the existing membership requirement.
Webshops can use the exported selections to restrict eligible shoppers; SnowBee's current
pricing lookup distinguishes members from non-members and does not enforce individual levels.
For B2C stores, status and inclusion in this response follow these timestamps. B2B stores
continue to follow the campaign's separate ordering-date window.
Product price responses also respect timed campaigns. Their upcomingPriceCalendar includes
future changes later today as well as changes on later dates. Each activatesAt is the actual
activation instant with the tenant's UTC offset, including daylight-saving changes. Refresh
cached product prices at those instants rather than waiting until the next midnight.
Schedule activation and expiry locally using the resolved timestamps. Passing a boundary does
not emit CampaignUpdated. Re-read campaign data when receiving campaign update notifications.
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 |
DUPLICATE_EMAIL, DUPLICATE_PHONE | Another customer already has this email address or phone number. Look that customer up instead of retrying. HTTP 409. |
VALIDATION_ERROR | A business rule rejected the change. Cart validation returns HTTP 422; an unhandled database business-rule rejection, such as a missing consignment price when creating an order, returns HTTP 400. The message explains the rule. |
CONFLICT | The cart is no longer active and cannot be changed. HTTP 409. |
NOT_FOUND | The cart or cart line does not exist in this store. HTTP 404. |
INVALID_UUID | A path ID is not a UUID |
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
- Build a storefront — the shopper's whole path on the cart, delivery, payment and login endpoints
- Events — Azure Service Bus notifications for product, inventory and order changes
- API Reference — every endpoint and field, tag E-commerce