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

# Customer Accounts

**Publié le** : 2026-08-14
**Catégorie** : Features

> Buyer accounts: two-step signup, login, the current-customer session and bookings, addresses, and marketing consent. Previous: [Guide 19](/docs/sdk/features/commerce-reservations)

> **Reference** - This guide is a tutorial. For exhaustive, always-current signatures and field lists, see the auto-generated reference (it regenerates on every `docs:sync`, so it never goes stale):
> 
> - SDK: [`CustomersService`](/docs/sdk/services/sdk-customers-service)
> - Types: [SDK Types](/docs/sdk/types)
> - API: [Storefront: Commerce](/docs/storefront-api/storefront-commerce)

## Prerequisites

- Lynkow SDK installed ([Quick Start](/docs/sdk/getting-started/quick-start))
- A **publishable key** on the client (see [Guide 18](/docs/sdk/features/commerce-products)). Customer endpoints reject a call without one.

```typescript
import { createClient } from 'lynkow'

const lynkow = createClient({
  siteId: '550e8400-e29b-41d4-a716-446655440000',
  publishableKey: 'lkw_pk_9f8g7h6j5k4w3m2n1p0qrstv',
})
```

The customer session is a **separate identity track** from the publishable key: the key identifies your app, the session token identifies the buyer. `login` and the activation step below return that token and store it in the client's in-memory state; authenticated methods then send it as `Authorization: Bearer <token>` alongside the key.

## Sign up (two steps)

Signup is deliberately two steps, and no session exists between them. This is what stops someone from setting a password on an address they do not control.

### Step 1: request an activation link

`lynkow.customers.register({ email })` takes the email and nothing else. It creates no session, does not sign anyone in, and only asks the API to mail an activation link. It resolves to `void`.

```typescript
await lynkow.customers.register({ email: 'alice@example.com' })

// Show ONE message on every outcome. Never branch on whether the email is known.
show('Check your inbox. If this address can be registered, a link is on its way.')
```

`register` resolves **identically** whether or not the email already has an account. Do not branch your UI on the result, and do not write a `catch` expecting a "already exists" rejection: doing either re-creates client-side the account-enumeration signal the API removed. A password, terms, or profile passed here has no effect, the server strips them.

### Step 2: activate and set the password

The emailed link lands on a page in your app (by default `/account/activate/[token]`, where the token is the last path segment). On that page, collect the password, terms acceptance, and any profile fields, then call `lynkow.customers.activation.confirm(...)`. This is the only point where the buyer has proven they control the address, so it is where credentials and consent are collected.

```typescript
// app/account/activate/[token]/page.tsx (client component)
// Returns CustomerAuthResult: { customer, token }
const { customer, token } = await lynkow.customers.activation.confirm({
  token: tokenFromTheUrl,   // the last path segment of the activation link
  password: 'a-strong-passphrase', // 8 to 100 chars
  acceptsTerms: true,       // MUST be true, ticked on THIS page by this buyer
  firstName: 'Alice',       // optional
  marketingOptIn: true,     // optional, unchecked by default
})

saveToken(token) // persist it (see "Keep the buyer signed in") - activation signs them in
```

`acceptsTerms` must be `true` or the server rejects the call and the account is not activated. Collect it (and the password) on this page, from the person completing it, never carried over from the form that called `register`. Activation signs the buyer in, so no follow-up `login` is needed, and the returned `token` is stored automatically on the client.

A link works exactly once. On any failure, `activation.confirm` throws a single uniform `LynkowError` that deliberately does not distinguish "never valid" from "already used". Route the buyer to your login page with wording that fits both, for example "This link is invalid or has already been used. If you already set your password, sign in." Do not tell the buyer the account does not exist, you cannot know that.

## Log in

`lynkow.customers.login({ email, password })` authenticates a returning buyer and returns `{ customer, token }`, storing the token on the client. Any failure (wrong password, unknown email) surfaces as one uniform `LynkowError` with code `'UNAUTHORIZED'`, with nothing that reveals whether the email exists.

```typescript
const { customer, token } = await lynkow.customers.login({
  email: 'alice@example.com',
  password: 'a-strong-passphrase',
})
saveToken(token)
```

## Keep the buyer signed in

The SDK holds the token in memory only, so a page reload or an SSR request starts without it. Persist the returned `token` in your own storage (for example an HttpOnly cookie) and seed it back when you create the client:

```typescript
const lynkow = createClient({
  siteId: '550e8400-e29b-41d4-a716-446655440000',
  publishableKey: 'lkw_pk_9f8g7h6j5k4w3m2n1p0qrstv',
  customerToken: savedToken, // restores the signed-in buyer
})
```

The SDK never persists or logs the token; that is your app's responsibility.

## The current buyer and their bookings

`lynkow.customers.me()` returns the signed-in buyer plus their most recent bookings. It requires a stored token and is not cached, since the booking list changes as the buyer books.

```typescript
// Returns CustomerSession: { customer, bookings }
// customer (partial): { id, email, firstName, emailVerified }
// each booking (partial): { reference, startsAt, status }
const { customer, bookings } = await lynkow.customers.me()

console.log(`${customer.email} has ${bookings.length} booking(s).`)
for (const booking of bookings) {
  console.log(`${booking.reference}: ${booking.status} at ${booking.startsAt}`)
}
```

Guest bookings placed with the same email before signup are retro-linked and appear here only once `customer.emailVerified` is `true`. Any reservation created while this session is active (see [Guide 19](/docs/sdk/features/commerce-reservations)) shows up here immediately.

## Log out

```typescript
await lynkow.customers.logout() // discard the token on this client only (no network call)
await lynkow.customers.logout({ everywhere: true }) // also revoke every server-side session
```

The default is a local token discard, so later calls go out without a Bearer header. `{ everywhere: true }` requires a stored token and first revokes every session issued to the buyer server-side. Clear your own persisted copy of the token at the same time.

## Address book

The nested `lynkow.customers.addresses` surface manages the signed-in buyer's saved addresses. All of it requires a stored token.

```typescript
// CustomerAddress (partial): { id, firstName, city, isDefaultShipping }
const address = await lynkow.customers.addresses.create({
  firstName: 'Alice',
  lastName: 'Martin',
  address1: '12 rue des Lilas',
  city: 'Lyon',
  postalCode: '69003',
  countryCode: 'FR',       // ISO 3166-1 alpha-2
  isDefaultShipping: true, // atomically unsets the previous default
})

const addresses = await lynkow.customers.addresses.list()
await lynkow.customers.addresses.update(address.id, { isDefaultShipping: true })
await lynkow.customers.addresses.remove(address.id)
```

`list()` follows every server page and returns a flat array. Setting a default atomically unsets the prior one, so re-read `list()` after an update to reflect both rows. Anti-spam fields are injected automatically on the writes.

## Marketing consent

The nested `lynkow.customers.marketingConsent` surface reads and updates the buyer's per-channel opt-in state. Each entry names a generic channel (for example `'whatsapp'` or `'email'`) with an `optedIn` boolean; opt-out always wins, and a channel with no record reads `false`.

```typescript
// Each entry: { channel, optedIn }
const { channels } = await lynkow.customers.marketingConsent.list()

// Opt out of one channel, or pass channel: 'all' to change every channel at once.
const after = await lynkow.customers.marketingConsent.update({
  channel: 'email',
  optIn: false,
})
console.log(after.channels)
```

## Forgotten password

The nested `lynkow.customers.passwordReset` surface handles a reset without a session. `request` mails a link (and, like `register`, always resolves the same way whether or not the email exists), and `confirm` sets the new password from the token in that link.

```typescript
await lynkow.customers.passwordReset.request({ email: 'alice@example.com' })
// ... buyer opens the emailed link ...
await lynkow.customers.passwordReset.confirm({
  token: tokenFromEmailLink,
  password: 'a-new-strong-passphrase',
})
```

Confirming does not sign the buyer in; they log in afterward with the new password.

## Reference

Do not treat this guide as the field list. For every method, signature, and property, link the source of truth:

- **SDK service:** [`CustomersService`](/docs/sdk/services/sdk-customers-service) - every method and signature.
- **Types:** [SDK Types](/docs/sdk/types) - the full `Customer`, `CustomerActivationConfirmInput`, `CustomerSession`, `CustomerAddress`, and marketing-consent shapes.
- **API:** [Storefront: Commerce](/docs/storefront-api/storefront-commerce) - request and response schemas.

## Next Steps

- [Guide 18: Product Catalog](/docs/sdk/features/commerce-products) - browse the products a signed-in buyer can reserve.
- [Guide 19: Reservations and Deposits](/docs/sdk/features/commerce-reservations) - create a booking that appears in the buyer's account.