# Customer accounts

URL: /docs/concepts/customers

Register and sign in shoppers with bearer tokens, verify email, reset passwords, manage the address book, show order history and delete an account.

Shoppers can create an account with the store to save addresses and see their orders. Accounts belong to one store: the same email can have separate accounts at two stores. The API uses bearer tokens, never cookies, so your storefront server keeps the token and sends it as `Authorization: Bearer …`.

## Endpoints [#endpoints]

| Endpoint | Does |
|---|---|
| `POST /v1/auth/register` | Creates an account and returns a token |
| `POST /v1/auth/login` | Signs in and returns a token |
| `POST /v1/auth/logout` | Revokes the current token |
| `GET /v1/auth/me` | The signed-in account |
| `PATCH /v1/auth/me` | Changes the name or email |
| `POST /v1/auth/verify` | Confirms an email address from the verification link |
| `POST /v1/auth/verify/resend` | Sends the verification email again |
| `POST /v1/auth/password/forgot` | Emails a password reset link |
| `POST /v1/auth/password/reset` | Sets a new password with the token from that link |
| `POST /v1/auth/password` | Changes the password of a signed-in account |
| `GET`, `POST /v1/account/addresses` | Lists or adds saved addresses |
| `PATCH`, `DELETE /v1/account/addresses/{id}` | Changes or deletes a saved address |
| `GET /v1/orders`, `GET /v1/orders/{reference}` | Order history |
| `DELETE /v1/account` | Erases the account |

## Register and sign in [#register-and-sign-in]

```bash tab="cURL"
curl -X POST https://api.elaraone.app/api/v1/auth/register \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "X-Cart-Token: $CART_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Katherine Johnson",
    "email": "katherine@example.com",
    "password": "correct-horse-battery",
    "password_confirmation": "correct-horse-battery"
  }'
```

```ts tab="TypeScript"
const res = await fetch('https://api.elaraone.app/api/v1/auth/login', {
  method: 'POST',
  headers: {
    'X-Tenant-Key': process.env.ELARAONE_TENANT_KEY!,
    'X-Tenant-Secret': process.env.ELARAONE_TENANT_SECRET!,
    'X-Cart-Token': guestCartToken, // optional: merges the guest's cart
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ email, password }),
})
const { data } = await res.json()
// Store data.token in an httpOnly cookie on your own domain, set to expire at data.expires_at.
```

```json title="Response"
{
  "data": {
    "user": {
      "id": 77,
      "name": "Katherine Johnson",
      "email": "katherine@example.com",
      "email_verified_at": null,
      "customer_id": 204
    },
    "token": "12|q8V…",
    "expires_at": "2026-11-23T10:12:31+00:00"
  }
}
```

- Registration returns `201` and login returns `200`, both with the same shape.
- Passwords need at least 8 characters, and `password_confirmation` must match.
- Wrong credentials, or an email already registered, return `422 VALIDATION_FAILED` with the field in `details`.
- The token can be used straight away, before the email is verified. `expires_at` says when it stops working.
- Send the guest's `X-Cart-Token` with login or registration to merge their basket into the customer's cart. An unknown or expired cart token is ignored; it never stops the sign-in.
- Register, login, password reset, profile changes and password changes share a tight rate limit: 10 a minute per store and IP. See [rate limits](/docs/concepts/rate-limits).

<Callout type="warn" title="Keep the token on your server">
Treat the bearer token like a password. Keep it in an httpOnly, secure cookie on your storefront's domain, and have your server add it to API calls. Never put it in `localStorage`.
</Callout>

Send the token on every call made for that customer, together with the store key and secret:

```bash
curl https://api.elaraone.app/api/v1/auth/me \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN"
```

A missing, expired or revoked token returns `401 UNAUTHENTICATED`. `POST /v1/auth/logout` revokes the token it was sent with.

## Change the name or email [#change-the-name-or-email]

`PATCH /v1/auth/me` takes `name`, `email` or both. Changing the email also needs `current_password`.

## Verify the email address [#verify-the-email-address]

When an account is created, the store sends a verification email, if the store has set its storefront's verify-email page in **Store settings**. The link opens that page on your storefront with a signed query string.

Your page sends that query string to the API exactly as it arrived, as the query string of `POST /v1/auth/verify`:

```ts title="On your verify-email page (server-side)"
// incomingUrl is the URL the shopper opened, e.g. https://hearthandoak.example/verify?expires=…&hash=…&id=…&signature=…
const query = new URL(incomingUrl).search // keep it byte for byte: same order, same encoding

const res = await fetch(`https://api.elaraone.app/api/v1/auth/verify${query}`, {
  method: 'POST',
  headers: {
    'X-Tenant-Key': process.env.ELARAONE_TENANT_KEY!,
    'X-Tenant-Secret': process.env.ELARAONE_TENANT_SECRET!,
  },
})
// 200 { "data": { "status": "verified" } }, or 403 when the link is invalid or has expired
```

The signature is checked against the exact query string, so do not re-encode, reorder or add parameters. `POST /v1/auth/verify/resend` (signed in) sends a fresh link, or answers `already-verified`.

## Reset a forgotten password [#reset-a-forgotten-password]

1. `POST /v1/auth/password/forgot` with `{ "email": "…" }`. It always answers `200` with `reset-link-sent`, whether or not the address has an account, so it cannot be used to find out who shops at the store.
2. The store emails a link to the reset page you set in **Store settings**, with the token and email filled in. The token works once, for 60 minutes. If the store has not set a reset page, no email is sent.
3. Your reset page sends `POST /v1/auth/password/reset` with `token`, `email`, `password` and `password_confirmation`. A bad or expired token returns `422 VALIDATION_FAILED` on `token`.

## Change the password [#change-the-password]

`POST /v1/auth/password` (signed in) takes `current_password`, `password` and `password_confirmation`. The token that made the request keeps working; every other token on the account is revoked, and `revoked_tokens` says how many.

## The address book [#the-address-book]

```json title="GET /v1/account/addresses"
{
  "data": [
    {
      "id": 42,
      "label": "Home",
      "title": null,
      "first_name": "Ada",
      "last_name": "Lovelace",
      "company_name": null,
      "line_one": "221B Baker Street",
      "line_two": null,
      "line_three": null,
      "city": "London",
      "state": null,
      "postcode": "NW1 6XE",
      "country_id": 235,
      "delivery_instructions": null,
      "contact_email": null,
      "contact_phone": "+447700900123",
      "shipping_default": true,
      "billing_default": true
    }
  ]
}
```

- `GET` lists the customer's addresses, newest first.
- `POST` creates one (`201`). `first_name`, `last_name`, `line_one` and `city` are required.
- `PATCH /v1/account/addresses/{id}` changes any field. `DELETE` removes the address.
- `label` is an optional nickname such as "Home" or "Work", up to 50 characters. `title` is the salutation.
- Setting `shipping_default` or `billing_default` to `true` moves that default to this address, so only one address holds each. Deleting a default moves it to the newest remaining address.
- Another customer's address id returns `404 NOT_FOUND`.

At checkout, a signed-in customer can use a saved address with `address_id`, or save a new one with `save_to_account`. See [carts](/docs/concepts/carts#addresses).

## Order history [#order-history]

`GET /v1/orders` lists the signed-in customer's orders, newest first, with `page` and `perPage` (1 to 100, default 24). `GET /v1/orders/{reference}` returns one order in the same shape checkout returns. See [checkout](/docs/concepts/checkout#the-order).

When a customer verifies their email, orders they placed earlier as a guest with that address become part of their account.

## Delete the account [#delete-the-account]

`DELETE /v1/account` lets a shopper erase their own account. It needs the bearer token and the account password as `current_password`:

```bash
curl -X DELETE "https://api.elaraone.app/api/v1/account?current_password=correct-horse-battery" \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN"
```

On success it answers `{ "data": { "status": "erased" } }`, and:

- the account can never sign in again, and every token on it is revoked, including the one that made the request;
- saved addresses and open carts are deleted;
- orders are kept as the store's records, with names, street, email and phone removed;
- guest orders, carts and form submissions made with the account's verified email are anonymised the same way;
- the verified email and phone numbers are kept only as one-way hashes on the store's do-not-contact list, so automated messages never reach them again.

A missing or wrong password returns `422 VALIDATION_FAILED`. `409 ACCOUNT_SHARED` means the account shares its customer record with other accounts (the store linked them), so only the store can erase it; nothing is changed. Deletion is rate limited to 3 a minute and 10 an hour per account, and 20 an hour per IP.

<Cards>
  <Card title="Carts" href="/docs/concepts/carts" description="Merge a guest cart at sign-in, and use saved addresses at checkout." />
  <Card title="Rate limits" href="/docs/concepts/rate-limits" description="The sign-in budget and how to handle 429." />
</Cards>
