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
/route on that domain.account/ callback - A server that keeps the customer session.
Configure customer sign-in
POST / 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"{
"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}/. loginMode is the mode
chosen in Settings → Checkout → Customer accounts → Sign-in:
loginMode | Form |
|---|---|
hosted | Auth page with the store's name and logo. |
storefront | Inside 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:
| Route | Purpose |
|---|---|
POST / | Verify email and password and continue the OAuth challenge. |
POST / | Send the code to create or recover a password. |
POST / | Verify that code before allowing a new password. |
PUT / | Save 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 /, compare state with the one in the cookie you set when sending the customer to sign in.
Then exchange code with POST /. 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>"
}'{
"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 / returns the current customer, or 401 if
the session expired.
| Response | Cause |
|---|---|
401 | The code expired or was used, codeVerifier doesn't match, or the account is inactive. |
409 | Customer.AccountConflict: the email belongs to another customer account in the store. |
Complete the registration
GET / 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 /.
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" }'{
"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, orrequired. hiddenfields are ignored even if you send them.- If no
requiredfield is missing,{}completes the registration. dateOfBirthusesYYYY-MM-DDand can't be later than today.- While registration is pending, account calls respond
403withCustomer.RegistrationRequired.
Show the orders
GET / 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 /
takes the orderId, not the number. It adds payments, shipments, and
statusTimeline.
Manage addresses
POST / 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"
}'| Action | Operation |
|---|---|
| List | GET / |
| Replace | PUT / |
| Make default | POST / |
| Delete | DELETE / |
PUT replaces the whole address: send all its fields. PUT, DELETE, and
default respond 204.
Edit the profile
GET / returns the profile: email, name, phone, ID document, and
addresses. PATCH / 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 / 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/. The SDK stores the
session in a secure cookie and uses the routes /,
/, and /.
| Step | SDK |
|---|---|
| Sign-in | startCustomerLogin and submitCustomerLogin in /, with CustomerLoginForm. |
| Session | completeCustomerLogin in /; customer and requireCustomer read it. |
| Registration | loadCustomerRegistration and submitCustomerRegistration, with CustomerRegistrationForm. |
| Orders | loadOrders and loadOrder. |
| Addresses | loadAddresses and addressAction. |
| Profile | loadProfile and profileAction. |
| Sign-out | logout in the / 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.
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
- Cart: create the cart with the customer session to apply their prices and codes.
- Authentication: sign-in modes and registration data.
- Customer account: profile, addresses, registration, and sessions.
- Customer orders: list, detail, and status.
Last updated: