> For the complete Lynkow documentation index in agent-friendly format, see [llms.txt](/llms.txt).

# Register a storefront customer account

**Publié le** : 2026-08-19
**Catégorie** : Commerce

## `POST /customers/register`

**Register a storefront customer account**

Takes an `email` and an optional `locale`, and NOTHING else. It ALWAYS returns an
identical generic 200 `{ ok: true }`, mails an activation link to the address, and
causes no other observable effect. It never installs a credential, never mutates an
existing row, and never issues a session token: an anonymous caller proves nothing
about the address they submit, so nothing they submit is allowed to cause an effect.
The password, terms acceptance, profile and marketing consent are collected FRESH on
the inbox-linked activation page (`activation/confirm`) from whoever proves they
control the address, and are applied there atomically.

The response is byte-identical on EVERY branch: a fresh address, an address backed by
an unclaimed guest row, an address that already has an account, an address whose
activation link is already live, a soft-deleted or GDPR-anonymized row, and a budget
exhaustion. There is no field-scoped duplicate-email 422 any more: that was a live
user-enumeration oracle, and it is DELETED rather than narrowed. Duplicate accounts
remain impossible via the partial-unique `(site_id, email)` index, so only the
disclosure went away. All email-dependent work (lookup, resolve-or-create, token
issuance, mail) runs in a DETACHED worker AFTER the response, so response latency
cannot leak whether the address exists either (no timing oracle), mirroring
`password-reset/request`. Optional `locale` selects the activation-mail language and is
never rejected: any value maps to `en`, `fr`, `es`, `de` or `pt`, a regional variant to its
base language (`pt-BR` gives `pt`) and any other non-empty string to English. Omitted,
empty, or not a string: the site's default locale is used and mapped the same way.

When (and only when) the worker CREATES the customer row, it also records a pending,
content-free welcome intent in the same transaction (commerce slice G1, ADR-0050). No
welcome mail is rendered or sent at this point: the account does not exist yet and the
activation link is already in flight, so the intent simply carries the resolved locale
until `activation/confirm` (or an earlier guest booking) promotes it. Reusing an
existing customer records nothing, and the whole side effect is inert while the
`customer_welcome_email` kill-switch is off. None of it is observable here: the
response, its latency, and the activation mail are all unchanged.

Behind the publishable key, the `customer_accounts` flag, spam protection, and the
customer-auth rate-limit bucket. Returns 200 with `{ ok: true }`.

> **Note:** This endpoint requires the e-commerce feature to be enabled on your site.

### Request Body

Content-Type: `application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | Required. Valid email address |
| `locale` | string | No | Buyer-email language. Never rejected: an exact match on en, fr, es, de or pt is kept, a regional variant falls back to its base language (pt-BR gives pt), and any other non-empty string falls back to English. Omitted, empty, or not a string: the site default language is used and mapped the same way |


### Responses

| Status | Description |
| --- | --- |
| `200` | Successful response |
| `401` | Missing or invalid storefront publishable key |
| `402` | The site subscription does not currently permit this operation |
| `403` | The storefront key origin is not allowed |
| `404` | Site, resource, or commerce feature not found |
| `422` | Validation error |


---