Skip to content
Online store

Customer accounts

Sign-in, registration, and the customer account in your frontend with the Storefront API.

Andrea signs in to your frontend with an email code or her email and password, and sees her orders, addresses, and details. This guide builds that account with the Storefront API. Your server keeps the session; the browser only gets a cookie.

Requirements

  • The store slug or a configured domain, for x-Store-Domain.
  • Your frontend on HTTPS, on an active domain of the store.
  • The /account/callback route on that domain.
  • A server that keeps the customer session.

Configure customer sign-in

POST /v1/customer/auth/config creates or updates the OAuth client for your frontend's origin. It returns the OAuth settings and the store's sign-in mode. It responds 404 if the origin isn't an active domain of the store.

curl -X POST "https://storefront.ecomiq.pe/v1/customer/auth/config?origin=https://mi-tienda.pe" \
  -H "x-Store-Domain: mi-tienda"
Response
{
  "authority": "https://auth.ecomiq.pe",
  "clientId": "ecomiq-customer-7204558912004325377",
  "authorizationEndpoint": "https://auth.ecomiq.pe/connect/authorize",
  "tokenEndpoint": "https://auth.ecomiq.pe/connect/token",
  "revocationEndpoint": "https://auth.ecomiq.pe/connect/revoke",
  "scope": "openid profile email api_storefront offline_access",
  "redirectUri": "https://mi-tienda.pe/account/callback",
  "loginMode": "hosted",
  "captcha": { "provider": "off", "siteKey": null, "isConfigured": true }
}

redirectUri is always {origin}/account/callback. loginMode is the mode chosen in Settings → Checkout → Customer accounts → Sign-in:

loginModeForm
hostedAuth page with the store's name and logo.
storefrontInside the store's theme.

Both modes offer email codes and passwords. Changing the mode moves the form; it does not change the account or its available sign-in methods.

Storefront mode in your frontend

The in-theme form is only available with the theme SDK. Your frontend uses the Auth page in both modes, and Auth shows the CAPTCHA if the store enabled it.

Send the customer to sign in

Generate random verifier, state, and nonce values. Store them in a short-lived HttpOnly cookie and redirect the browser to authorizationEndpoint. Use clientId, redirectUri, and scope as returned.

import { createHash, randomBytes } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const state = randomBytes(32).toString("base64url");
const nonce = randomBytes(32).toString("base64url");

const authorize = new URL(config.authorizationEndpoint);
authorize.search = new URLSearchParams({
  client_id: config.clientId,
  response_type: "code",
  redirect_uri: config.redirectUri,
  scope: config.scope,
  code_challenge: createHash("sha256").update(verifier).digest("base64url"),
  code_challenge_method: "S256",
  state,
  nonce,
}).toString();

Auth asks for the email. Send code sends a 6-digit code that expires in 5 minutes; Sign in with password lets the customer use that account's password. If the account has two-factor verification, Auth also requires an authenticator code or a recovery code before signing in.

After verifying the customer, Auth returns to redirectUri with code and state. If the URL has error, start over. Both methods complete the same OAuth Authorization Code flow with PKCE; do not use grant_type=password.

Auth remembers the customer in that browser for up to 7 days. Add prompt=login to the URL to ask them to authenticate again.

Create or recover a password

Create or recover password first verifies the email with a code. The customer then enters and confirms a new password. If the account has two-factor verification, they must complete it too; the email code does not replace that second factor. Customers can continue signing in with an email code after creating a password.

In hosted mode, Auth presents these steps. In storefront mode, CustomerLoginForm presents them inside the theme. The integration keeps the same OAuth client, redirectUri, state, nonce, and PKCE throughout the flow.

The SDK uses these routes on the Auth authority for the embedded form:

RoutePurpose
POST /v1/customer-auth/password-challengesVerify email and password and continue the OAuth challenge.
POST /v1/customer-auth/password-setupsSend the code to create or recover a password.
POST /v1/customer-auth/password-setups/verifyVerify that code before allowing a new password.
PUT /v1/customer-auth/passwordSave the new password and complete any remaining verification.

These routes do not issue tokens through a password grant. Let the SDK manage the challenges, then complete the callback as shown below.

Create the session

In /account/callback, compare state with the one in the cookie you set when sending the customer to sign in. Then exchange code with POST /v1/customer/sessions. Send the same nonce: without it, the call responds 401.

curl -X POST https://storefront.ecomiq.pe/v1/customer/sessions \
  -H "x-Store-Domain: mi-tienda" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "<code>",
    "codeVerifier": "<verifier>",
    "redirectUri": "https://mi-tienda.pe/account/callback",
    "nonce": "<nonce>"
  }'
Response
{
  "sessionId": "kq8Vd2xN4mW7rT1yB6sF3hJ9pL0cZ5aE8uG2iO4nR7w",
  "expiresAt": "2026-11-03T15:30:00Z",
  "customer": {
    "id": "7204558912004325301",
    "firstName": null,
    "lastName": null,
    "email": "andrea.ramos@example.com",
    "status": "active"
  }
}

If the email had no customer in the store, the session creates one. If they already bought as a guest with that email, it links them.

Store sessionId in an HttpOnly, Secure cookie that expires at expiresAt. The session also expires after 7 days without use. Never expose it to browser JavaScript.

On every account call, send sessionId in X-Customer-Session. Don't add Authorization: with both, the call responds 400. GET /v1/customer/sessions/current returns the current customer, or 401 if the session expired.

ResponseCause
401The code expired or was used, codeVerifier doesn't match, or the account is inactive.
409Customer.AccountConflict: the email belongs to another customer account in the store.

Complete the registration

GET /v1/customer/registration returns the fields the store asks for, their values, and requiresCompletion. When it's true, show the form before the account or checkout. Send the details with POST /v1/customer/registration.

curl https://storefront.ecomiq.pe/v1/customer/registration \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID"

curl -X POST https://storefront.ecomiq.pe/v1/customer/registration \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Andrea", "lastName": "Ramos", "phone": "+51987654321" }'
GET response
{
  "fields": {
    "firstName": "required",
    "lastName": "required",
    "phone": "optional",
    "dateOfBirth": "hidden"
  },
  "firstName": null,
  "lastName": null,
  "phone": null,
  "dateOfBirth": null,
  "requiresCompletion": true
}
  • Each field is hidden (not asked), optional, or required.
  • hidden fields are ignored even if you send them.
  • If no required field is missing, {} completes the registration.
  • dateOfBirth uses YYYY-MM-DD and can't be later than today.
  • While registration is pending, account calls respond 403 with Customer.RegistrationRequired.

Show the orders

GET /v1/customer/orders lists the customer's purchases, newest first. It includes guest purchases made with the same email.

curl "https://storefront.ecomiq.pe/v1/customer/orders?limit=20" \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID"

curl https://storefront.ecomiq.pe/v1/customer/orders/7204558912004325401 \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID"

Each row is a purchase with orderId, orderNumber (such as ORD-2026-0001), status, total, and items. If it was split across sellers, orderIds lists each order. While hasNextPage is true, send nextCursor in cursor.

GET /v1/customer/orders/{orderId} takes the orderId, not the number. It adds payments, shipments, and statusTimeline.

Manage addresses

POST /v1/customer/addresses adds an address and responds 201 with its id. addressKey, countryIsoCode, and addressLine are required. The first address becomes the default.

curl -X POST https://storefront.ecomiq.pe/v1/customer/addresses \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "addressKey": "Casa",
    "countryIsoCode": "PE",
    "addressLine": "Av. José Larco 1150",
    "department": "Lima",
    "province": "Lima",
    "district": "Miraflores",
    "cellPhone": "+51987654321"
  }'

PUT replaces the whole address: send all its fields. PUT, DELETE, and default respond 204.

Edit the profile

GET /v1/customer returns the profile: email, name, phone, ID document, and addresses. PATCH /v1/customer updates it and responds 204.

curl -X PATCH https://storefront.ecomiq.pe/v1/customer \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Andrea",
    "lastName": "Ramos",
    "phone": "+51987654321",
    "identificationType": "DNI",
    "identificationNumber": "45678912",
    "entityName": null,
    "entityIdentificationNumber": null
  }'

Resend the ID document

If you omit identificationType, identificationNumber, entityName, or entityIdentificationNumber, PATCH clears them. Resend the values GET returned.

firstName, lastName, and phone keep their value if you don't send them. Changing phone turns off smsConsent. The email can't be edited: it's the sign-in email.

Sign out

DELETE /v1/customer/sessions/current revokes the session and responds 204. Then delete your cookie.

curl -X DELETE https://storefront.ecomiq.pe/v1/customer/sessions/current \
  -H "x-Store-Domain: mi-tienda" \
  -H "X-Customer-Session: $SESSION_ID"

Auth still remembers the customer in that browser. On the next sign-in, add prompt=login to ask them to authenticate again.

With the theme SDK

Ecomiq themes do these steps with @ecomiq/storefront. The SDK stores the session in a secure cookie and uses the routes /account/login, /account/callback, and /account/register.

StepSDK
Sign-instartCustomerLogin and submitCustomerLogin in /account/login, with CustomerLoginForm.
SessioncompleteCustomerLogin in /account/callback; customer and requireCustomer read it.
RegistrationloadCustomerRegistration and submitCustomerRegistration, with CustomerRegistrationForm.
OrdersloadOrders and loadOrder.
AddressesloadAddresses and addressAction.
ProfileloadProfile and profileAction.
Sign-outlogout in the /account/logout action.

In storefront mode, CustomerLoginForm offers email codes, passwords, and password creation or recovery through an email code. It also presents the CAPTCHA if the store enabled it and two-factor verification if the account requires it. requireCustomer redirects to sign-in or pending registration.

app/routes/account/callback.ts
import { completeCustomerLogin } from "@ecomiq/storefront";
import type { Route } from "./+types/callback";

export async function loader({ request, context }: Route.LoaderArgs) {
  return completeCustomerLogin(request, context);
}

Next steps

Last updated:

On this page