The reference implementation ships with the Nova template, in
src/components/account/ and src/commerce/account-route.ts. Start from those files rather than from a blank page.Where the account lives
There is exactly one account route.
The ten section names are
overview, orders, downloads, subscriptions, rewards, referrals, favorites, support, settings and reseller.
Your storefront is a single-page app, so serve /dashboard from your own router and read the two parameters:
How the session works
Sign-in is a single-use code sent by email. There is no password, and there is no redirect: the customer types the code into your page and the same page becomes their account.1
The customer asks for a code
requestOtp(email) sends the address and nothing else.2
Shoppex emails the code
The code is short lived. Its lifetime and retry limits are enforced on the server.
3
The customer types it back
verifyOtp(email, otp) completes the sign-in.4
The edge sets the session cookie
The Shoppex storefront worker sets an HttpOnly cookie on your own domain, scoped to that host. Every later call carries it automatically.
No token in JavaScript
Nothing hands your code a session token, and nothing should store one. The cookie is HttpOnly, so your scripts cannot read it even by accident.
No shop parameter
The shop is derived from the host the buyer is on. Never send a shop, a shop id or a slug with an account call.
Same origin only
Calls go to your own domain under
/api/customer/*. Anything that changes data must come from your own pages; the edge refuses cross-site writes before they reach the API.The account calls
Load the Storefront SDK as you already do for the catalog and the cart. In a code storefront that is the CDN global (window.shoppex), wrapped by src/commerce/shoppex.ts in the official templates. In a bundled project, the same functions are named exports of @shoppexio/storefront.
Every call answers with the same envelope: { success, data?, message?, code? }. message is a sentence to show the customer. code is the machine-readable reason, when the server sent one, and is the only value you should branch on.
Licences, subscriptions and tickets have no list call. All three arrive
inside
dashboard(), and the calls above act on one item whose id you already
have. GET /api/customer/licenses is not an endpoint and answers 404 at the
edge — the same is true for /subscriptions and /tickets.dashboard(), orders() and order() are validated against the published response contract before you see them. A payload that does not match comes back as a failure rather than as half-read data, so a change on the platform side surfaces as an error instead of as a wrong number on a customer’s screen.
Sign a customer in
Only a
401 means “not signed in”. Every other failure — a 403, a 5xx,
a contract mismatch, or a network error that carries no status at all —
means the answer is unknown. Rendering the sign-in form there tells a buyer
whose session is still live that they were signed out, and a reload
contradicts you. Show the error and let them retry.Never do the opposite either and treat a failed check as a signed-in customer:
that renders an account shell around data nobody fetched.The overview
One call carries the whole overview. Read what you need out of it and count nothing yourself.data.invoices holds the most recent orders, which is enough for a “recent orders” block without a second request.
Order history
Pagination is page based. Ask for a page and a size, and readhas_more to decide whether a next page exists.
uniqid, and that is the id the detail call takes:
Two details that bite
Field casing is not uniform. The dashboard payload is snake_case (total_display, created_at, line_items), while the order list and the order detail are camelCase (totalDisplay, createdAt, lineItems). Keep the names as they arrive. Renaming them in a shared helper is how one of the two surfaces quietly starts reading undefined.
total and currency are not a pair. currency is the currency the buyer sees. The total field is a normalized figure in USD, and total_display / totalDisplay is the same total in the buyer’s currency. Render the display amount first:
discount and discountDisplay. Line prices, subtotal and tax are already in the buyer’s currency.
What a storefront cannot do
The edge keeps a fixed list of account endpoints, bound to their method. Anything outside it answers404 before it reaches the API, whatever the page calls it from. Your theme runs on the same origin as the customer’s session, so that list is the answer to what shop-authored code may do on a buyer’s behalf, and it is not something a theme can widen.
Deliberately closed today:
Link a customer to the hosted flow for anything on that list rather than trying to reach it from your theme.
Failures and empty states
Account pages fail in ways catalog pages do not: the session expires, the network drops mid-page, a shop turns a feature off. Three habits keep that honest.- Show the server’s sentence.
messageis written for the customer. A friendlier guess of your own is often simply wrong, and a customer who is told the wrong reason cannot fix anything. - Tell the three states apart. Loading, empty and failed are different. An error rendered as an empty list reads as “you have no orders”, which is the one thing it does not mean.
- Never fill a gap. If the API did not send a value, do not compute a replacement. A number your storefront made up is worse than a missing one, because nobody can tell.
Where to go next
Code storefront development
Project structure, the commerce layer, and working locally.
Storefront SDK reference
Every SDK call, including the catalog, cart and checkout surfaces.