> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless customer accounts

> Build your own customer login, order history, order detail, and downloads with Shoppex data and no custom backend.

Use `@shoppexio/storefront/customer` when your storefront runs on your own domain and you want to build the customer panel yourself. Shoppex handles email OTP login, tenant isolation, orders, and download authorization. Your frontend owns the UI.

<Note>
  Headless customer accounts require an active Business plan. They reuse the same publishable key and allowed origins as [Headless Checkout](/developers/headless/checkout).
</Note>

## Setup

Open **Dashboard -> Settings -> Developer -> Headless Checkout**. Generate a publishable key and add the exact origin that hosts the customer page, for example `https://yourstore.com`.

```bash theme={"system"}
npm install @shoppexio/storefront
```

```ts theme={"system"}
import { createCustomerClient } from '@shoppexio/storefront/customer';

const customer = createCustomerClient({
  shop: 'your-shop-slug',
  publishableKey: 'pk_live_your_key',
});
```

The publishable key is browser-safe. Never use a secret `shx_` Developer API key in this client.

## Client methods

| Method                               | Use                                                                    |
| ------------------------------------ | ---------------------------------------------------------------------- |
| `requestOtp(email)`                  | Sends a one-time code to the buyer.                                    |
| `verifyOtp(email, code, options)`    | Verifies the code and saves the customer session.                      |
| `getSession()`                       | Returns the saved session, or `null` when no valid session exists.     |
| `me()`                               | Returns the buyer profile and shop branding.                           |
| `orders(options)`                    | Returns one page of order summaries.                                   |
| `order(invoiceId)`                   | Returns the full order detail and delivery data.                       |
| `download(invoiceId, downloadIndex)` | Returns `{ blob, filename, contentType }` for one authorized file.     |
| `logout()`                           | Revokes the current headless session and removes it from the browser.  |
| `clearSession()`                     | Removes only the local session. It does not revoke the server session. |

## OTP login

```ts theme={"system"}
await customer.requestOtp('buyer@example.com');

await customer.verifyOtp('buyer@example.com', '123456', {
  rememberMe: true,
});
```

Normal login is stored in `sessionStorage` and is valid for up to 12 hours. `rememberMe: true` stores it in `localStorage` and keeps the customer signed in for up to 30 days. Both are capped by the underlying Shoppex buyer session. A logout or an unauthorized response removes the saved session.

Use `rememberMe` only after the buyer explicitly selects a **Keep me signed in** option on a private device.

OTP requests use a success-like response for known and unknown email addresses. Do not use the response to decide if a customer account exists.

Check the saved session when your customer page starts:

```ts theme={"system"}
const session = customer.getSession();

if (session) {
  const profile = await customer.me();
  console.log(profile.customer);
} else {
  // Show your OTP login form.
}
```

## Customer and orders

```ts theme={"system"}
const profile = await customer.me();

const firstPage = await customer.orders({
  page: 1,
  limit: 20,
  status: 'COMPLETED',
  search: 'invoice-id',
});

const order = await customer.order('invoice-uuid');
```

The client validates the `me`, order-list, and order-detail response contracts before returning them. Prices remain decimal strings so your UI does not introduce floating-point rounding.

`orders()` returns summary rows for an order list. Each summary contains at most three line items. Do not use the summary as a full order.

Call `order()` when the buyer opens one order. The detail response contains pricing, payment, line-item, and delivery data.

Use `pagination.has_more` to decide if your UI must show a **Load more** button. The API returns at most 50 orders per page.

### Order detail data

Use decimal strings from the response. Do not convert money to JavaScript floating-point values before calculations.

```ts theme={"system"}
const order = await customer.order('invoice-uuid');

const displayAmount = order.totalDisplay ?? order.total;
const paymentName = order.currentPayment?.displayName
  ?? order.currentPayment?.apmMethod
  ?? (order.currentPayment?.cryptoGateway ? 'Crypto payment' : null);

console.log(displayAmount, order.currency, paymentName);
```

Use `displayName` before `apmMethod`. The `cryptoGateway` field contains the exact coin when both labels are null. Format that coin with your buyer-facing labels. Do not show the raw `gateway` value because it can identify an internal payment rail.

The order detail contains these main groups:

| Data            | Fields                                                                                  |
| --------------- | --------------------------------------------------------------------------------------- |
| Price           | `subtotal`, `discount`, `tax`, `balancePaidAmount`, `total`, `totalDisplay`, `currency` |
| Payment         | `currentPayment`, `payments`, `gatewayUpdatedAt`                                        |
| Items           | `lineItems` with product, variant, quantity, price, and delivery data                   |
| Additional data | `customFields`, `statusDetails`, and order timestamps                                   |

Use `currentPayment` for the active payment method. `payments` contains the same public payment snapshot in array form for existing consumers.

## Delivery and service instructions

Call `order()` before you build the delivery section. The order-list response does not contain delivery data.

Each item in `order.lineItems` contains these delivery fields:

| Field                     | Use                                                                     |
| ------------------------- | ----------------------------------------------------------------------- |
| `deliveryStatus`          | Shows if delivery is pending, in preparation, delivered, or delayed.    |
| `deliveredAt`             | Contains the delivery time after delivery.                              |
| `deliveryText`            | Contains instructions that belong to this delivered order item.         |
| `product.serviceText`     | Contains the default service instructions for the delivered product.    |
| `deliverySummary.codes`   | Contains license keys and other delivered codes.                        |
| `deliverySummary.serials` | Contains serials and replacement state.                                 |
| `deliverySummary.notes`   | Contains delivery notes.                                                |
| `deliverySummary.links`   | Contains external access links or a `downloadIndex` for a Shoppex file. |

Shoppex returns `deliveryText` and `product.serviceText` only after the item is delivered. This rule prevents delivery content from appearing before payment and fulfillment.

Use the order-specific `deliveryText` first. Use the product `serviceText` when the order has no specific instructions.

```ts theme={"system"}
import type { HeadlessCustomerOrder } from '@shoppexio/storefront/customer';

type OrderItem = HeadlessCustomerOrder['lineItems'][number];

function getDeliveryInstructions(item: OrderItem): string | null {
  return item.deliveryText?.trim()
    || item.product?.serviceText?.trim()
    || null;
}

const order = await customer.order('invoice-uuid');

for (const item of order.lineItems) {
  if (item.deliveryStatus === 'AWAITING_FULFILLMENT') {
    console.log(`${item.productTitle}: Being prepared by the seller`);
    continue;
  }

  if (item.deliveryStatus === 'FAILED') {
    console.log(`${item.productTitle}: Delivery is delayed`);
    continue;
  }

  if (item.deliveryStatus !== 'DELIVERED') continue;

  const instructions = getDeliveryInstructions(item);
  console.log(instructions);
  console.log(item.deliverySummary?.codes ?? []);
  console.log(item.deliverySummary?.serials ?? []);
  console.log(item.deliverySummary?.notes ?? []);
  console.log(item.deliverySummary?.links ?? []);
}
```

`deliveryText` and `serviceText` can contain rich-text HTML. Sanitize this HTML before you use `dangerouslySetInnerHTML` or a similar API.

Open an external delivery link only when `href` is not null. Use `rel="noopener noreferrer"` when the link opens a new browser tab.

Each serial has these fields:

| Field            | Use                                               |
| ---------------- | ------------------------------------------------- |
| `value`          | Contains the serial value.                        |
| `is_replacement` | Shows that the serial replaced an earlier serial. |
| `is_replaced`    | Shows that a newer serial replaced this serial.   |

<Note>
  The Shoppex editor does not edit a self-hosted customer panel. Your site must render these fields in its own order-detail component.
</Note>

### Delivery status behavior

| Status                 | Recommended UI                                                          |
| ---------------------- | ----------------------------------------------------------------------- |
| `PENDING`              | Do not show delivery content or promise a delivery message.             |
| `AWAITING_FULFILLMENT` | Show that the seller is preparing the item.                             |
| `DELIVERED`            | Show instructions, codes, serials, notes, and links.                    |
| `FAILED`               | Show that delivery is delayed and tell the buyer to contact the seller. |

This delivery status belongs to one line item. It is separate from the payment status of the complete order.

## Downloads

```ts theme={"system"}
const order = await customer.order('invoice-uuid');
const downloadIndex = order.lineItems
  .flatMap((item) => item.deliverySummary?.links ?? [])
  .find((item) => item.downloadIndex !== undefined)?.downloadIndex;

if (downloadIndex === undefined || downloadIndex === null) {
  throw new Error('This order has no downloadable file');
}

const file = await customer.download(order.uniqid, downloadIndex);
const url = URL.createObjectURL(file.blob);

const link = document.createElement('a');
link.href = url;
link.download = file.filename ?? 'download';
link.click();
URL.revokeObjectURL(url);
```

Shoppex checks that the invoice belongs to the signed-in buyer before it returns the file. Always pass the `downloadIndex` returned by `order()`. The SDK removes hosted-portal-only download URLs so a self-hosted site never follows a broken `/api/customer/*` link or receives an unsafe aggregate HTML launcher.

## Logout and session state

```ts theme={"system"}
const activeSession = customer.getSession();

await customer.logout();
customer.clearSession();
```

`logout()` revokes only this external customer session. It does not sign the buyer out of a separate Shoppex-hosted customer portal tab. If the network request fails, the SDK keeps the local token so your UI can retry the revocation; a successful logout or an already-unauthorized response clears it.

Use `clearSession()` only when you must remove local state without a server request. For normal logout, call `logout()` by itself.

## Error handling

Use `HeadlessCustomerError` to handle expected API errors. A `401` response clears an expired or rejected session.

```ts theme={"system"}
import { HeadlessCustomerError } from '@shoppexio/storefront/customer';

try {
  const order = await customer.order('invoice-uuid');
  console.log(order);
} catch (error) {
  if (error instanceof HeadlessCustomerError && error.status === 401) {
    // Return the buyer to your OTP login form.
  } else if (error instanceof HeadlessCustomerError) {
    console.error(error.status, error.code, error.message);
  } else {
    console.error('The customer request failed', error);
  }
}
```

## Implementation checklist

* Build the OTP request and OTP verification forms.
* Add a buyer-controlled **Keep me signed in** option.
* Use `me()` for the signed-in buyer and shop branding.
* Use `orders()` for the order list and pagination.
* Use `order()` for pricing, payment, and delivery details.
* Render delivery state for every line item.
* Use `download()` only with a returned `downloadIndex`.
* Handle `401` by returning the buyer to the login form.
* Add a logout action that calls `logout()`.

## Security boundary

Every customer request must pass all of these checks:

* valid `pk_live_` or `pk_test_` publishable key
* exact browser `Origin` in the shop's allowed-origin list
* active Business plan
* valid `hcs_` customer session bound to the same origin and shop
* invoice ownership for order detail and downloads

The SDK is intentionally read-only. It exposes profile data, order history, order detail, downloads, and logout. Customer mutations, wallet actions, subscription changes, reseller orders, and merchant APIs are not available through this token.
