Skip to content

Customer accounts & sign-in

litecommerce 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 litecheckout 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 raw value exists only in flight, never in a database row. verify issues it two ways at once so either surface can use it:

  • Cookie — an httpOnly, Secure, SameSite=Lax __Host- cookie that the API sets for the hosted litecheckout surface. The browser sends it automatically and JavaScript can't read it. Because it's a __Host- cookie it is origin-bound — a BYO frontend on a different origin can't use it and should rely on the bearer token instead.
  • Bearer token — the same session returned in the response body, for a BYO frontend that holds the token server-side and sends Authorization: Bearer <token> itself.

Use the cookie on hosted litecheckout; use the bearer for a BYO backend that proxies customer calls. Either way, keep the token server-side — never expose it to client JavaScript.

Sign-in flow (passwordless)

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.litecommerce.io/api/v1/public/customer/auth/request-link \
  -H "x-organization-slug: your-store" \
  -H "Content-Type: application/json" \
  -d '{ "email": "shopper@example.com" }'

Both always return a 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 which emails have accounts. Don't branch your UI on it — show "check your email" either way.

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.litecommerce.io/api/v1/public/customer/auth/verify \
  -H "x-organization-slug: your-store" \
  -H "Content-Type: application/json" \
  -d '{ "email": "shopper@example.com", "code": "123456" }'

On success it mints a fresh session: sets the __Host- cookie (hosted) and returns the bearer token + expiresAt + customerId in the body (BYO). A bad or expired token/code is a neutral failure — the issue-and-verify surface is rate-limited per tenant so the OTP can't be brute-forced.

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.litecommerce.io/api/v1/customer/account/profile \
  -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/:orderNumber — one order's detail (items, totals, status).

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

  • GET /customer/account/orders/:orderNumber/returns — return activity for an order.
  • POST /customer/account/orders/:orderNumber/return-actions — start a return for eligible items.

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 litecheckout renders all of the above for you at {tenant}.litecheckout.io/account — the customer signs in and manages their account with no integration work. See Hosted litecheckout.
  • 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.