Customer accounts & sign-in
Storefront requests also carry their current origin: browsers supply Origin;
SSR and backend transports forward x-storefront-origin. Domains selects Live
or Sandbox data automatically, including token-only calls. Missing or conflicting
context is rejected, with no Live fallback. See storefront data selection.
cartonium gives each tenant's shoppers a passwordless customer account: they sign in with a magic link or a one-time code (no password to store or reset), and the session unlocks their profile, saved addresses, order history, and returns. The same API backs both hosted cartonium checkout and a BYO storefront.
Customer accounts are separate from merchant accounts. A merchant user (Owner/Admin/Staff) signs in to the admin with a Supabase JWT; a customer signs in to the storefront/checkout surface with a customer session. They are different identities, different tokens, and different endpoints.
All paths below are under /api/v1. Every call carries x-organization-slug —
customer identities are tenant-scoped, so a session minted for one tenant is
meaningless to another.
The session credential
A customer session is an opaque token, hashed at rest (ADR-007). The API's
verify response returns the raw bearer once and also sets an httpOnly,
Secure, SameSite=Lax __Host- cookie on the API response's origin.
- Hosted cartonium checkout verifies through its own same-origin server route. That route writes the token into the hosted origin's httpOnly cookie and returns expiry metadata to browser JavaScript, without echoing the bearer. A cookie set by a direct call to the API origin does not establish a session on the hosted origin.
- BYO storefronts verify through their own backend, retain the bearer
server-side, and proxy customer calls with
Authorization: Bearer <token>. Use a secure session mechanism between your browser and backend; origin-bound cookies do not transfer between storefront, API, and hosted checkout origins.
Keep the customer-session bearer server-side. Do not expose it in client JavaScript, browser storage, URLs, or logs. Merchant JWTs and tenant API keys do not replace this customer credential.
Sign-in flow (passwordless)
These examples use the Preview API deployment and a storefront address registered in the tenant's Production entry in Domains. Replace the example origin with that verified address; it selects Live commerce data on this API deployment. There is no default-to-Live fallback, and the Preview API hostname does not select Sandbox. Challenge delivery still requires an allowed recipient and configured delivery policy; external email is disabled in Sandbox. See Environments & schema identity.
1. Request a challenge
Two issuers, both anonymous (no session yet), both under
/api/v1/public/customer/auth:
POST /public/customer/auth/request-link— sends a one-click magic-link email, with a fallback OTP.POST /public/customer/auth/request-otp— sends a short-lived numeric one-time code.
curl -X POST https://api.preview.litecommerce.io/api/v1/public/customer/auth/request-link \
-H "x-storefront-origin: https://shop.your-store.example" \
-H "x-organization-slug: your-store" \
-H "Content-Type: application/json" \
-d '{ "email": "shopper@example.com" }'Successful issuance requests return the same neutral 200 regardless of
whether the email belongs to a customer of this tenant. This is deliberate:
the response can't be used to enumerate accounts. Don't branch your UI on it
— show "check your email" either way. Validation, rate-limit, and availability
errors still need their normal error handling; a neutral response is not
proof of inbox delivery.
2. Verify and start a session
POST /public/customer/auth/verify accepts either shape:
{ "token": "<magic-link-token>" }— from the emailed link, or{ "email": "...", "code": "<otp>" }— from the one-time code.
curl -X POST https://api.preview.litecommerce.io/api/v1/public/customer/auth/verify \
-H "x-storefront-origin: https://shop.your-store.example" \
-H "x-organization-slug: your-store" \
-H "Content-Type: application/json" \
-d '{ "email": "shopper@example.com", "code": "123456" }'On success the API mints a fresh session and returns token, expiresAt,
idleExpiresAt, persistent, and customerId. Use the server-side transport
described above. A bad or expired token/code returns a neutral 401; the
issue-and-verify surface also has rate limits. Do not infer the account's
existence from a failed verification.
Session lifetime and persistence
The current staging contract enforces two deadlines:
| Verify choice | Idle window | Absolute window |
|---|---|---|
Omit persistSession, or send false | 6 hours | 24 hours |
Send literal boolean persistSession: true after an affirmative "Keep me signed in" choice | 30 days | 365 days |
expiresAt is the fixed absolute deadline. idleExpiresAt advances on
authenticated use, capped by expiresAt; the session expires when either
deadline passes. Cookie retention does not override those server checks, and
logout or revocation can end access earlier. Do not infer persistence from a
remembered email, browser state, or a previous session. This is an API option,
not a claim that every hosted sign-in UI offers a persistence control.
3. Use the session
Authenticated calls live under /api/v1/customer/* and require the session
(cookie or bearer) plus x-organization-slug:
curl https://api.preview.litecommerce.io/api/v1/customer/account/profile \
-H "x-storefront-origin: https://shop.your-store.example" \
-H "x-organization-slug: your-store" \
-H "Authorization: Bearer <session-token>"POST /customer/auth/logout ends the session (clears the cookie / revokes the
token).
The account API
All authenticated, all tenant-scoped to the signed-in customer — a session can only ever read or write its own data.
Profile
GET /customer/account/profile— read name, email, phone, email-verified state.PATCH /customer/account/profile— update the customer's own profile fields.
Addresses
A customer keeps an address book with default shipping/billing flags.
GET /customer/account/addresses— list saved addresses.GET /customer/account/addresses/:id— read one.POST /customer/account/addresses— add an address.PATCH /customer/account/addresses/:id— update an address.DELETE /customer/account/addresses/:id— remove an address.
Order history
GET /customer/account/orders— paginated list of the customer's orders.GET /customer/account/orders/by-reference/:orderReference— one order's detail (items, totals, status), addressed by its stable publicorderReference.GET /customer/account/orders/:orderNumber— the same detail addressed by the display order number (supported compatibility route).
Every order response carries two identities:
orderReference(or1_+ 32 lowercase hex) — the stable, customer-safe public address for the order. Use it in URLs and handoffs. It is not a credential: possession alone never grants access, and unknown, malformed, cross-tenant, and unowned references all return the same neutral404after authentication.nullonly on legacy orders that predate the reference backfill.orderNumber— the human-facing display number. Show it to customers; don't use it as a routing identity in newly generated URLs.
Orders are matched to the customer, so the list only ever contains their own orders — there's no cross-customer lookup on this surface.
Returns
By-reference routes are the primary family for new integrations; the order-number routes remain supported for compatibility.
GET /customer/account/orders/by-reference/:orderReference/returns— return activity for an order.POST /customer/account/orders/by-reference/:orderReference/returns— create a return (no customer email in the request; the session is the identity).POST /customer/account/orders/by-reference/:orderReference/returns/:returnId/cancel— cancel a pending return on that order.POST /customer/account/orders/by-reference/:orderReference/return-actions— issue a purpose-bound return action token for eligible items. The raw token is returned once in the response — nothing is emailed — and the caller consumes it at/customer/actions/:token(hosted cartonium checkout navigates straight to its/action/{token}page).GET /customer/account/orders/:orderNumber/returnsandPOST /customer/account/orders/:orderNumber/return-actions— the same reads and return-action start, addressed by display number (compatibility).
Bookings
In active development: these routes are present in the current staging contract. Bookings are not launch-complete; the shipped customer-account capability does not make every booking action available in Production.
GET /customer/account/bookings— paginated booking history, optionally by status.GET /customer/account/bookings/:id— customer-safe schedule, delivery, policy actions, documents, agreements, and Sales/payment summary.POST /customer/account/bookings/:id/cancel-request— apply or request cancellation under the current policy.POST /customer/account/bookings/:id/reschedule-request— request a changed window while preserving the booked duration.GET /customer/account/booking-groups— paginated atomic-group history.GET /customer/account/booking-groups/:publicRef— one owned group's detail.POST /customer/account/booking-groups/:publicRef/resume— mint a fresh scoped resume proof for an owned group.
The server decides whether a booking action is available and whether it is immediate or merchant-reviewed. See Booking availability, holds & lifecycle.
Saved payment methods
Saved-card account management uses the customer session and tenant context,
under /customer/payment-methods:
GET /customer/payment-methods— list customer-safe card metadata.POST /customer/payment-methods/setup-intent— create a SetupIntent and return itsclientSecret,stripeAccountId, and setup correlation fields.PATCH /customer/payment-methods/:id/default— choose a default saved card.DELETE /customer/payment-methods/:id— remove a card;409means an active payment dependency blocks removal.
Collect card data through Stripe Payment Element, initialized for the returned connected account. Keep card numbers and CVC out of cartonium requests. Creating or confirming a SetupIntent is not a checkout payment. Persisted card state comes from the setup webhook or the list endpoint's bounded reconciliation; a browser callback alone does not prove the card was saved.
The current staging implementation additionally provides a reviewed account-card setup through three routes. This extension remains pending exact-build hosted and PC-29 acceptance; it is separate from basic saved-card management and is not released hosted adoption guidance:
POST /customer/payment-methods/setup-attempts/:setupHandle/review— bind the exact SetupIntent and reviewed billing copy before confirmation, with separatesaveAddressandmakeDefaultBillingchoices.GET /customer/payment-methods/setup-attempts/:setupHandle?setupIntentId=…— read that exact setup'ssetupStatus,cardProjection,addressSave, anddefaultBillingoutcomes from the server.POST /customer/payment-methods/setup-attempts/:setupHandle/address-save— consume the still-valid reviewed address choice after proven setup success.
Keep the returned setup handle paired with its exact SetupIntent. A billing correction starts a new setup; it does not edit an existing review. Saving an address is separate from saving a card, selecting a default card, or changing a shipping address. Do not repeat an uncertain address-save write or replay lost consent after a reload: read saved cards and addresses and follow the reported outcome. See the API reference for request fields and outcome meanings.
Account card management does not activate the separate dormant saved-card checkout flow.
Tokenized actions (no sign-in required)
Some customer actions are reached from an emailed link rather than a signed-in
session — the link carries an opaque, single-purpose token (the a token
family, ADR-007: hashed at rest, expiring, tenant-scoped). This is how, for
example, a "start your return" email works without forcing a login.
GET /customer/actions/:token— preview what the token will do (the action + its context). A safe read, no mutation.POST /customer/actions/:token— consume the token and perform the action, once.
Unknown, expired, revoked, and cross-tenant tokens all return the same neutral
404 — the endpoint never reveals whether a token is real.
The read-only order-status link (the o token, sent with the
order-confirmation email) follows the same hash-at-rest pattern but is a pure
read — see Order status links.
Hosted vs BYO
- Hosted cartonium checkout provides the optional account UI: profile, addresses,
order history, returns, and saved payment methods. Link to the resolved
account URL and let that origin establish its own customer session. Booking
extensions retain the staging qualification above. The hosted exact-order
destination is
/account/orders/by-reference/{orderReference}: link a customer's order there with theorderReferencefrom any API response. Hosted sign-in never puts a customer email in a URL; the sign-in page's code entry returns to that exact order, while an ordinary emailed sign-in link currently lands on hosted order history. See Hosted cartonium checkout. - BYO storefront calls these endpoints directly. Proxy them through your own
backend so the session token (bearer) stays server-side; never call
/customer/*from client JavaScript with the raw token.
Related
- Hosted cartonium checkout — the cartonium-hosted account + checkout surface
- Order status links — the post-purchase
o-token status page - BYO checkout API — the checkout-session contract a custom storefront drives
- Booking availability, holds & lifecycle — public booking search/hold/checkout and customer change actions
- Auth & roles — how merchant auth differs from customer sessions