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 owns | The storefront owns |
|---|---|
| Catalog facts: products, variants, images, stock, prices, campaigns and offers | Pages, navigation, search UI, merchandising |
| The cart and its pricing, member prices included | The cart id, in a cookie |
| Delivery options and pickup points, relayed live from the merchant's carrier | Letting the shopper choose one |
| The payment session, its verification and the capture | Mounting Adyen's Drop-in with what the session returns |
| Customers, orders and order history | Who 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:
GET /customers/by_email/:emailwith the verified email; on404,PUT /customers/:customerIdwith a new UUID, the email and the name to create the customer.PUT /carts/:cartId/customerwith{ "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/customermakes the cart anonymous again at logout.- Decide membership: the customer's
loyaltyProgramsentry whoseidequals the store'sloyaltyProgram.idfromGET /store, withenrolled: true. That decides whether the catalog showsloyaltyMemberPrice; the cart is priced by SnowBee on the same fact. - Keep the customer id, email, name and membership in a signed
httpOnlysession 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:
- An event says what changed, not what it changed to. It names a subject and an entity id, and you read the current state back through the API. That is deliberate: a redelivery is harmless, and an event that arrives out of order cannot apply stale data over fresh.
- Delivery is at least once. Make your handler idempotent. Every event carries a unique
eventIdif you would rather deduplicate.
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:
| Subject | What to do |
|---|---|
ProductUpdated | Re-read the product and rewrite its document. If it comes back missing or no longer published for this store, delete the document instead |
InventoryUpdated | Re-read only if it affects what the web shop sells -- see below |
CampaignUpdated | Re-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 of | Your platform keeps |
|---|---|
| Products, variants, images, prices, campaigns, stock and pickup availability | The storefront, navigation, merchandising and content |
| Order intake, picking, shipment and capture | The basket and checkout experience, if you keep your own |
| Customers, loyalty membership and order history | Sessions, 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.
- Send
campaignCodeon every line sold under a campaign, and SnowBee checks the price bears it out. A normal campaign line must be priced at that SKU'scampaignPricefromGET /campaigns; a three-for-two line at the blended per-unit price a cash register would reach. A code the price does not support is rejected rather than quietly accepted. externalOrderIdandexternalOrderNumbercarry your platform's own references. A repeat of either is answered with a conflict instead of a second order, so a retry after a timeout is safe, and the number you show the shopper is the one the merchant sees in SnowBee.
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
- The store token is requested server-side and refreshed before it expires; the browser never holds it
- Cart ids and customer ids travel in cookies or request bodies, never in URLs
- Listing and schema calls are cached for minutes; the cart is read fresh
- The freight block sent at checkout is the one the payment session was opened with
- The order page checks that the order belongs to the logged-in shopper before rendering it
429responses honourRetry-After
If you consume events, also:
- The webhook signature is verified before the body is parsed, with a constant-time comparison
- Handlers are idempotent, and a redelivery of an event you already applied changes nothing
- A failed delivery is retried or dead-lettered rather than dropped, and someone is alerted when a transport fails repeatedly
- A full re-read runs on a schedule, so a missed event cannot leave a copy wrong indefinitely