Skip to content

Customer accounts and personal prices

Sign customers in with an emailed code, keep their customer token on your server, and show each signed-in customer their own prices and stock, live from MAM, in the Storefront API, GraphQL, the SDK and CMS Orbit blocks.

How-to guide7 min read

Customers can have an account in the shop. They sign in with their email address and a 6-digit code, so there is no password, and the same code makes the account the first time. While they are signed in, every price and stock figure you ask for on their behalf is theirs: their company's price list, or, when they or their company are linked to a MAM account, that account's own prices and live stock per branch.

The login is always the email address. A MAM account number is never a login and is never taken from what a customer types: the shop's team links it to the customer or their company in Commerce Orbit, after checking it with MAM. See Customers and companies.

Sign a customer in

Both calls go to the cart endpoints' allowed websites only (Website and Allowed websites in Settings) or come from your server. Call them from your server where you can, so the token never reaches the browser.

POST/commerce/api/public/northwilds/customer/code with { "email": "kari.nordmann@northwilds.com", "locale": "nb" } emails a code that works for 10 minutes. It always answers { "sent": true }, so it never tells anyone whether an address has an account. It allows a few tries a minute per address and per IP address.

POST/commerce/api/public/northwilds/customer/session with { "email": "…", "code": "123456", "name": "Kari Nordmann" } signs them in. name is only used for a new account. Add "cart": "c_…" to move the customer's cart over to them. Five wrong codes end the code. The answer is 201 for a new account and 200 otherwise:

JSON
{ "data": { "token": "cst_…", "expiresAt": "2026-11-28T09:00:00.000Z", "created": false, "customer": { "email": "kari.nordmann@northwilds.com", "name": "Kari Nordmann", "company": { "name": "Fjellguidene AS", "terms": "invoice", "termsDays": 30 }, "account": { "number": "NW1001", "status": "linked", "onStop": false, "creditLimit": 5000000, "balance": 1240000, "branch": "OSL", "branchName": "Oslo warehouse" }, "pendingRequest": null, "priceList": "Trade 15%" } } }

The token lasts 30 days and each use extends it. Keep it like a password: in an httpOnly cookie on your own domain, never in a link or localStorage. Send it back in the X-Customer-Token header.

GET/commerce/api/public/northwilds/customer returns the same customer for a token, and 401 when it has expired.

DELETE/commerce/api/public/northwilds/customer/session signs out and ends the token. Add ?cart=c_… to take the customer's prices off the cart too.

GET/commerce/api/public/northwilds/customer/subscriptions lists the customer's subscriptions: id, status (active, pastDue, cancelling or cancelled), every (such as "Every month"), amount, currency, lines (title, variantTitle, quantity), nextAt (the next payment, or the end for one that is cancelling), orders and createdAt. DELETE/commerce/api/public/northwilds/customer/subscriptions/{id} cancels one at the end of the period paid for, and answers with it.

GET/commerce/api/public/northwilds/customer/downloads lists the files the customer bought, newest order first, each with reference, filename, title, url (works for 7 days), downloads, limit, expiresAt and usable.

POST/commerce/api/public/northwilds/customer/link-request with { "accountNumber": "NW1002", "note": "I buy for Turlaget Nord" } asks the shop's team to link an account. Nothing changes for the customer until someone in the team approves it.

The customer's account

Every call below needs the customer's token in X-Customer-Token, and answers 401 without a valid one.

PATCH/commerce/api/public/northwilds/customer with { "name": "Kari Nordmann", "phone": "+47 912 34 567", "marketing": true } changes their details. Send only what changes. The email address can't be changed, as it is the sign-in. The answer is the customer, which also carries phone, marketing, addresses and deletionRequestedAt.

GET/commerce/api/public/northwilds/customer/orders?limit=20 lists their orders, newest first, up to 100: the reference, status, payment and sending status, lines, totals, tracking, reorder (the product and variant ids and quantities to add to a cart for Order again), whether a receipt can be downloaded and the invoice number. Download it from /commerce/api/public/northwilds/orders/NW1042/pdf?email=….

GET/commerce/api/public/northwilds/customer/addresses lists the address book. POST the same address with { "name": "Kari Nordmann", "company": null, "line1": "Storgata 12", "line2": null, "postalCode": "0155", "city": "Oslo", "region": null, "country": "NO", "phone": null, "defaultShipping": true } adds one, PATCH…/customer/addresses/{id} changes one, including defaultShipping and defaultBilling, and DELETE removes it. Every answer is the whole book, each address with its id, defaultShipping and defaultBilling. An account keeps up to 20 addresses, and the addresses the customer orders with are added by themselves.

POST/commerce/api/public/northwilds/customer/deletion asks the shop's team to delete the account. The team gets a notification and closes the account; the orders stay for their accounts.

Carts take a billingAddress next to shippingAddress. Without one, the order is billed to the delivery address.

Personal prices in the Storefront API and GraphQL

Send X-Customer-Token with any product, collection, cart or checkout call. The answer is then the customer's, marked Cache-Control: private, no-store, so a CDN never shares it. A call with a customer token from a website that isn't allowed gets 403, and an unknown or expired token 401.

Each variant then has, besides price:

listPrice
Means
The shop's normal price, to show struck through.
priceSource
Means
account (from MAM for their account), priceList (their company's price list), shop (the normal price), loading (MAM is still answering) or unavailable (MAM didn't answer in time).
branches
Means
For a MAM account: code, name, quantity and yours, live stock per branch with the customer's own branch marked. null otherwise.

The product has pricing: source, account and checkedAt. With loading or unavailable, price is the shop's own price: show it as such, never as the customer's, and ask again a moment later. Account prices are only in the shop's main currency; in other currencies the customer sees the shop's prices.

GraphQL has the same fields on Variant and Product, and customer on Query, when the request sends the header. See GraphQL.

Prices for products you already show

Pages built with CMS Orbit, or any page you cache, show the shop's normal prices to everyone. Fetch the signed-in customer's prices after the page has loaded and lay them over it:

POST/commerce/api/public/northwilds/prices with { "productIds": ["6ae2…", "6ae3…"], "branches": "yours" } and the customer's token. Up to 100 products. "branches": "all" adds every branch. GET…/prices?ids=… works too. It answers account, priceList, onStop, checkedAt and products, each with id, slug, currency, source, price (min, max), available and variants. It allows 30 calls a minute per customer.

With the SDK

@bifrost-orbit-cms/sdk has the calls and a helper that lays the prices over CMS Orbit's commerce fields:

TypeScript
import { commerceStorefront, commerceStorefrontUrl, withPersonalPrices } from "@bifrost-orbit-cms/sdk";const shop = commerceStorefront({ baseUrl: "https://app.bifrostorbit.com/commerce/api/public/northwilds" });await shop.sendCode("kari.nordmann@northwilds.com");const { token } = await shop.signIn({ email: "kari.nordmann@northwilds.com", code: "123456" });const prices = await shop.prices(token, items.map((item) => item.id));const shown = withPersonalPrices(items, prices);

commerceStorefrontUrl(product.url) works out baseUrl from a product's hosted shop address. In React, useCommercePrices(items, load) from @bifrost-orbit-cms/sdk/react returns the items with personal prices once load has answered, and the normal ones until then; pass null as load for visitors who aren't signed in.

The pattern on a CMS Orbit website

The Northwilds demo website does it in three parts:

  1. A route on your own server, such as /api/shop/[...path], passes sign-in, customer, link-request and prices on to Commerce Orbit. After sign-in it keeps the token in an httpOnly cookie and sets a second, readable cookie that only says someone is signed in.
  2. Pages stay cached and public. CMS Orbit delivers the Product grid block with the shop's prices for everyone.
  3. The block asks for personal prices in the browser when the readable cookie is there, with one call to /api/shop/prices for every product on the page, and shows "Your price" and stock at the customer's branch.

One call for a whole page keeps it cheap: Commerce Orbit asks MAM for up to 50 parts at once, keeps each account's answers for the time set in the MAM settings (90 seconds unless the shop changes it), and shares one MAM call between requests that arrive together.

Was this page helpful?

Read next