Skip to content

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 choiceIdle windowAbsolute window
Omit persistSession, or send false6 hours24 hours
Send literal boolean persistSession: true after an affirmative "Keep me signed in" choice30 days365 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 public orderReference.
  • 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 neutral 404 after authentication. null only 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/returns and POST /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 its clientSecret, stripeAccountId, and setup correlation fields.
  • PATCH /customer/payment-methods/:id/default — choose a default saved card.
  • DELETE /customer/payment-methods/:id — remove a card; 409 means 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 separate saveAddress and makeDefaultBilling choices.
  • GET /customer/payment-methods/setup-attempts/:setupHandle?setupIntentId=… — read that exact setup's setupStatus, cardProjection, addressSave, and defaultBilling outcomes 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 the orderReference from 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.