# 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.

Source: https://docs.bifrostorbit.com/commerce/developers/customer-accounts/

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](https://docs.bifrostorbit.com/commerce/everyone/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`:

| Field | Means |
| --- | --- |
| `listPrice` | The shop's normal price, to show struck through. |
| `priceSource` | `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` | 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](https://docs.bifrostorbit.com/commerce/developers/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:

```ts
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.
