Documentation Build a storefront

Build a storefront Experimental

A web shop on SnowBee needs no commerce logic of its own. SnowBee prices the cart with the same rules as the cash register, asks the carrier what delivery costs, takes the payment through Adyen, turns the cart into an order and captures the money when the order is picked. The storefront renders pages, keeps two cookies and calls the API from its server. This guide walks that path in the order a shopper meets it. Endpoint details live on the E-commerce page; the API reference has every field.

The split

SnowBee ownsThe storefront owns
Catalog facts: products, variants, images, stock, prices, campaigns and offersPages, navigation, search UI, merchandising
The cart and its pricing, member prices includedThe cart id, in a cookie
Delivery options and pickup points, relayed live from the merchant's carrierLetting the shopper choose one
The payment session, its verification and the captureMounting Adyen's Drop-in with what the session returns
Customers, orders and order historyWho the shopper is, and a signed session cookie saying so

The storefront's server holds the store's client credentials and calls the API with the store token. The browser never sees the token, a cart id, or a customer id in a URL.

1. Know the store

GET /store returns the store's name, currency, the loyalty program member prices refer to, and whether checkout takes payment. Read it once at startup and cache it; it changes rarely.

{
  "id": "019c1234-5678-7abc-8def-123456789001",
  "name": "bjorklund.no",
  "currencyIsoCode": "NOK",
  "loyaltyProgram": { "id": "019c1234-5678-7abc-8def-123456789060", "name": "Bjørklund Kundeklubb" },
  "paymentRequired": true
}

2. Browse

GET /products/search is the listing: sellable products as summaries, filterable by brand, category and product groups, searchable, sortable, paged with cursors both ways. The dimension ids and names come from the product_schema endpoints; a menu of product groups and brands is two calls, cacheable for minutes.

Every summary carries three prices and the offers that cover the product:

{
  "price": { "price": "2999", "campaignPrice": "2499", "loyaltyMemberPrice": "2249" },
  "offers": [
    { "campaignId": "019c1234-5678-7abc-8def-123456789030", "code": "TFT-AUTUMN", "name": "3 for 2 on knitwear",
      "campaignType": "THREE_FOR_TWO", "loyaltyProgramMembershipRequired": false }
  ]
}

Show campaignPrice when present, with price struck through. Show loyaltyMemberPrice as the shopper's price only when they are logged in and a member of the store's loyalty program; to everyone else it is a hint of what logging in is worth. A THREE_FOR_TWO offer has no unit price, so show it as a badge; the cart applies it once the basket qualifies. An offer with loyaltyProgramMembershipRequired is for members only.

GET /products/:productId gives the product page: variants with colour and size, images, the description, the same prices and offers. GET /skus/:skuId/inventory lists where a variant is in stock, for a reserve-in-store button that calls POST /click_and_collect.

3. Cart

Create a cart on the first add, keep its id in an httpOnly cookie, and treat 404 on the cart as "start a new cart": carts are deleted at checkout and after 30 days idle. Every cart call returns the whole cart, priced, with names, variant names and image URLs on each line, so a cart page needs no further calls. See Build a cart.

Prices on the cart are what the shopper pays: campaign prices applied, 3 for 2 blended across the line once it qualifies, member prices when the cart belongs to a member (step 6).

4. Delivery

POST /carts/:cartId/delivery_options with the shopper's address returns the carrier products the merchant's Profrakt agreements offer for this cart, priced live, sorted by gross freight. Options with pickupPointRequired need a pickup point from POST /carts/:cartId/pickup_points. The chosen option becomes the freight block that both the payment session and the checkout take. See Delivery options.

5. Pay and check out

When paymentRequired is true, POST /carts/:cartId/payment_session opens an Adyen session for the cart's total plus the chosen freight. Mount Adyen's Drop-in with the session, and when it completes, POST /carts/:cartId/checkout with the addresses, the freight block, and the paymentId plus the Drop-in's sessionResult. SnowBee verifies the authorisation against what the cart costs now, creates the order and deletes the cart. The capture happens in SnowBee when the order is picked. See Paying for the cart.

Redirect payment methods such as Vipps send the shopper away and back to your returnUrl; keep the address, the freight and the session in the browser's session storage so the checkout can finish where it left off.

A store without web payment skips the session: checkout creates the order straight away.

6. Login, membership and order history

SnowBee does not run logins. The storefront verifies who the shopper is with an identity provider (the reference storefront uses Google sign-in and verifies the id token against its own OAuth client id) and then acts for that shopper with the store token:

  1. GET /customers/by_email/:email with the verified email; on 404, PUT /customers/:customerId with a new UUID, the email and the name to create the customer.
  2. PUT /carts/:cartId/customer with { "customerId": "..." } to bind the shopper's cart to them. If the customer already has another active cart in the store, its lines are folded in (this cart wins on a shared SKU) and the other cart is deleted, so a shopper has one cart across devices. DELETE /carts/:cartId/customer makes the cart anonymous again at logout.
  3. Decide membership: the customer's loyaltyPrograms entry whose id equals the store's loyaltyProgram.id from GET /store, with enrolled: true. That decides whether the catalog shows loyaltyMemberPrice; the cart is priced by SnowBee on the same fact.
  4. Keep the customer id, email, name and membership in a signed httpOnly session cookie.

From then on, a cart created for a logged-in shopper should be bound on creation, and checkout should send customerId too, so the order is placed for them whatever the cart's state.

GET /customers/:customerId/orders is the account page's order list. It includes the store's guest orders delivered to the customer's email, so a shopper who bought before they had a login finds those orders once their email is known. GET /orders/:orderId is the order page: lines, delivery address, freight, shipments with tracking, payments. Show an order only when its customerId is the shopper's or its delivery email is the shopper's verified email, and answer 404 otherwise so order ids reveal nothing.

Reservations from the product page should send the shopper's customerId as well; without it SnowBee matches the reservation on the email or phone typed in.

7. Confirmation

The checkout response carries orderId and orderNumber. Show the order number; SnowBee sends no email to the shopper, so say where the order can be found instead of promising one. The order's status is PLANNED when received, RELEASED while it is being handled, CLOSED when done.

Staying in step

Everything above reads from SnowBee when a shopper asks. If you keep a copy of the catalog -- a search index, a read model, another ecommerce platform -- you need to know when it goes stale.

SnowBee notifies you when catalog facts change. Subscribe a store over an Azure Service Bus queue, a signed HTTPS webhook, or both, in the eCom store's settings in the SnowBee application. The Events page has the full list of what is sent, the message shape and how to verify a webhook signature.

Two properties shape how you consume them:

A subscription receives every event by default. Narrow it to the subjects you handle, per transport, so you are not woken for orders when you only keep a catalog.

Keeping a search index current

The common case. Index one document per product. GET /products/search gives you most of it in bulk -- name, brand, category and the two group levels, the price with its campaign and member variants, the in-stock flag, the main image and any offers -- and pages by cursor, so filling the index for the first time is one walk through that endpoint.

It does not give you everything. SKU ids, dynamic attribute values and the description live on GET /products/{id}, so a searchable index costs one detail call per product on top of the walk. Fetch them in small batches rather than one at a time.

Three subjects then keep the index current:

SubjectWhat to do
ProductUpdatedRe-read the product and rewrite its document. If it comes back missing or no longer published for this store, delete the document instead
InventoryUpdatedRe-read only if it affects what the web shop sells -- see below
CampaignUpdatedRe-read the products the campaign covers, from GET /campaigns

The product-schema subjects -- BrandsUpdated, MainProductGroupsUpdated and the rest -- change the labels you facet on rather than the products themselves. Subscribe to them if you cache those lists; skip them if you read the names off each product.

Inventory is the noisy one, and most of it is not yours. A merchant with shops offering click and collect generates an inventory event for every sale on every shop floor, and none of those change what the web shop can sell. The metadata tells you which is which:

"metadata": {
  "productId": "019c1111-2222-7abc-8def-333344445555",
  "affectsEcomStock": "false",
  "affectsClickAndCollect": "true"
}

Drop anything where affectsEcomStock is false and you will discard the large majority without a single call. productId saves you keeping your own SKU-to-product mapping, since the event names a SKU and your document is a product.

Re-index on a schedule as well. Events keep the index fresh; the sweep keeps it correct. It covers the case events do not: a campaign starting or ending changes prices without anything being saved, so no event fires. Read the inclusive validFromAt and exclusive validToAt from GET /campaigns and schedule refreshes at those boundaries; campaigns can now start and end within the day. Keep a periodic sweep as recovery for missed refreshes. To rebuild from scratch after an outage, POST /products/trigger_events re-emits ProductUpdated for the whole assortment rather than making you page the catalog again.

Prices in your index may lag by moments, and that is fine. The cart quote is authoritative, so a shopper who adds a product is charged what SnowBee prices it at, not what your index said. Show the indexed price, never trust it at checkout.

Nothing here is particular to one search engine, but two things are worth knowing if you index Norwegian. Turn on decompounding for the searchable text, or a shopper searching sølvsmykke will miss every product named smykke i sølv. And the search engine is a good place to answer questions the API does not: the reference storefront finds similar products by asking its index for the nearest neighbours in product group, brand and attributes, which costs one query instead of a catalog trawl.

Running another ecommerce platform

Nothing above assumes the storefront is the one described in this guide. A merchant already running an ecommerce platform -- Centra, Shopify or otherwise -- can keep it and let SnowBee be the source of catalog and stock rather than replacing either side.

The division is the same as the table at the top, one layer up:

SnowBee is the source ofYour platform keeps
Products, variants, images, prices, campaigns, stock and pickup availabilityThe storefront, navigation, merchandising and content
Order intake, picking, shipment and captureThe basket and checkout experience, if you keep your own
Customers, loyalty membership and order historySessions, accounts and anything else you already own

Fill your platform by paging the catalog once, then subscribe to the events to stay in step. Do not poll the catalog on a timer: a full read is expensive and still leaves you minutes behind, and the events exist so you do not have to.

Orders come back with POST /orders. Keep your own checkout and price the basket yourself: each line carries the SKU, the quantity, netUnitPrice after any markdown, and the VAT rate. Two fields are worth knowing before you build it.

If you would rather not price anything, use the SnowBee cart from step 3 instead and let it quote. It applies member prices and multi-buy offers for you, at the cost of moving the basket to SnowBee.

Reference storefront

SnowBee maintains a reference storefront that does exactly this: Astro on Vercel, one deployment per store, configured by five environment variables plus two for login. It is deliberately plain and exists to prove the API and to be copied from.

It also carries a worked version of this page's second half. The same catalog is browsable two ways, straight from the API and through an Algolia index, so the two can be compared on the same data; a webhook endpoint verifies the signature and refreshes single records as events arrive; and a standalone script fills the index from nothing. Ask your SnowBee contact for access.

Go-live checklist

If you consume events, also: