# Contactaccounts

Contacts geven je storefront contactaccounts: registratie, inloggen, wachtwoordherstel, een profiel en opgeslagen adressen. Alle routes leven onder `/v1/api/contacts`, met een identieke alias op `/v1/api/customers` voor integraties die vóór de hernoeming zijn gebouwd. Contacts horen bij elke winkel, dus je hoeft vooraf geen module in te schakelen.

## Alias: `/v1/api/customers`

Elke route hieronder wordt ook, ongewijzigd, aangeboden op het equivalente `/v1/api/customers/...` pad (bijvoorbeeld `POST /v1/api/customers/register` naast `POST /v1/api/contacts/register`). Request- en response-vorm zijn op beide identiek. Deze alias is permanent, geen deprecatievenster: bestaande integraties hoeven nooit te migreren, al zouden nieuwe integraties `/v1/api/contacts` moeten gebruiken.

## Twee tokens, twee headers

Contact-endpoints gebruiken een gelaagd authenticatiemodel:

1. **Tenant-API-key** (altijd verplicht). Elk request draagt de API-key van je winkel als `Authorization: Bearer {api_key}`, precies zoals de rest van de storefront API. De key heeft de `contacts:write` scope nodig voor alle POST-, PATCH- en DELETE-endpoints en de `contacts:read` scope voor de GET-endpoints. Een key die vóór de hernoeming is aangemaakt draagt in plaats daarvan `customers:write` / `customers:read`, en wordt nog steeds geaccepteerd. Een ontbrekende scope retourneert `403 {"message":"Invalid ability provided."}`.
2. **Contactsessietoken** (verplicht op contact-geauthenticeerde endpoints). `POST /v1/api/contacts/register` en `POST /v1/api/contacts/login` retourneren een `token`-string. Stuur deze op volgende requests mee in de `X-Customer-Token` header. Een ontbrekend of ongeldig contacttoken retourneert `401 {"message":"Invalid customer token."}`.

De twee headers zijn onafhankelijk en beide verplicht op contact-geauthenticeerde routes:

```
Authorization: Bearer {api_key}
X-Customer-Token: {token}
```

Welke endpoints wat nodig hebben:

| Endpoint | API-key scope | X-Customer-Token |
| --- | --- | --- |
| `POST /register` | `contacts:write` (`customers:write` geaccepteerd) | Nee |
| `POST /login` | `contacts:write` (`customers:write` geaccepteerd) | Nee |
| `POST /forgot-password` | `contacts:write` (`customers:write` geaccepteerd) | Nee |
| `POST /reset-password` | `contacts:write` (`customers:write` geaccepteerd) | Nee |
| `POST /logout` | `contacts:write` (`customers:write` geaccepteerd) | Ja |
| `GET /me` | `contacts:read` (`customers:read` geaccepteerd) | Ja |
| `PATCH /me` | `contacts:write` (`customers:write` geaccepteerd) | Ja |
| `GET /me/addresses` | `contacts:read` (`customers:read` geaccepteerd) | Ja |
| `POST /me/addresses` | `contacts:write` (`customers:write` geaccepteerd) | Ja |
| `PATCH /me/addresses/{id}` | `contacts:write` (`customers:write` geaccepteerd) | Ja |
| `DELETE /me/addresses/{id}` | `contacts:write` (`customers:write` geaccepteerd) | Ja |

Contactsessietokens verlopen niet op een timer. Ze worden ingetrokken door `POST /logout` (één token) en door een succesvol wachtwoordherstel (alle tokens van het contact).

## Het publieke contactobject

Contactgerichte endpoints retourneren deze vorm (interne velden zoals `is_guest`, `type` en `customer_group_id` worden nooit blootgesteld op de storefront API):

| Veld | Type | Beschrijving |
| --- | --- | --- |
| `id` | string (uuid) | Contact-id. Stabiel over de gast-naar-account-upgrade heen. |
| `name` | string | Volledige naam. |
| `email` | string of null | E-mailadres, altijd lowercase indien aanwezig. Uniek per winkel. Registreren via de storefront vereist altijd een e-mailadres; een contact kan alleen een null e-mailadres hebben wanneer het door de handelaar is aangemaakt (bijvoorbeeld een zakelijk contact toegevoegd via het klantdashboard of CSV-import) zonder e-mailadres. |
| `phone` | string of null | Telefoonnummer. |
| `kind` | string | `person` of `company`. Alleen-lezen via deze API: wordt door de handelaar ingesteld, niet door de storefront. |
| `company_name` | string of null | Bedrijfsnaam. Alleen ingesteld wanneer `kind` `company` is. Alleen-lezen via deze API. |
| `registration_number` | string of null | KvK-nummer of vergelijkbaar registratienummer. Alleen-lezen via deze API. |
| `vat_id` | string of null | Btw-nummer. Alleen-lezen via deze API. |
| `external_id` | string of null | Door de handelaar toegekende identifier, bijvoorbeeld een ERP- of boekhoud-id. Alleen-lezen via deze API. |
| `display_name` | string | De bedrijfsnaam wanneer die er is, anders de naam van het contact. Afgeleid, niet opgeslagen. |
| `created_at` | string (ISO 8601) | Aanmaaktijdstempel, bijvoorbeeld `2026-08-14T10:32:11+00:00`. |

`kind`, `company_name`, `registration_number`, `vat_id` en `external_id` zijn leesbaar op elk contact maar niet schrijfbaar via `register` of `PATCH /me`: een storefrontaccount kan zichzelf niet omzetten in een zakelijk contact. De handelaar stelt deze velden in vanuit de Contacten-pagina's van het klantdashboard.

## Gastlevenscyclus

E-mailcapture in de winkelwagen (Commerce-module) kan een contactrij aanmaken met alleen een e-mailadres en geen wachtwoord (een gast). Dit is op drie plekken van belang:

- **Registratie upgradet gasten.** Registreren met een e-mailadres dat bij een gastrij hoort, upgradet die rij ter plekke: zelfde `id`, bestelgeschiedenis behouden. Alleen een e-mailadres dat bij een echt (niet-gast) account hoort, faalt met `422 "Email already registered."`.
- **Gasten kunnen niet inloggen.** Ze hebben geen wachtwoord, dus inloggen faalt met de generieke `422 "Invalid credentials."`.
- **Wachtwoordherstel upgradet ook.** Het voltooien van een reset zet een wachtwoord, markeert het account als niet-gast, en zet `email_verified_at` als dat nog niet gezet was. Dit is een tweede pad van gast-checkout naar een volwaardig account.

## Rate limits

Alle contact-endpoints delen de storefront-limiet van 120 requests per minuut per winkel (429 met een `Retry-After` header bij overschrijding). De vier credential-endpoints (`register`, `login`, `forgot-password`, `reset-password`) dragen een extra limiet van 10 requests per minuut per client-IP om brute-force-pogingen af te remmen.

## Fouten

Alle fout-envelopes zijn de standaard storefront-vormen:

| Status | Body | Oorzaak |
| --- | --- | --- |
| 401 | `{"message":"Unauthenticated."}` | Ontbrekende of ongeldige tenant-API-key. |
| 401 | `{"message":"Invalid customer token."}` | Ontbrekende of ongeldige `X-Customer-Token` op een contact-geauthenticeerde route. |
| 403 | `{"message":"Invalid ability provided."}` | API-key mist de vereiste scope. |
| 403 | `{"message":"Tenant suspended.","code":"tenant_suspended"}` | Winkel geschorst. |
| 404 | `{"message":"..."}` | Resource niet gevonden (of niet van het geauthenticeerde contact). |
| 422 | `{"message":"...","errors":{"field":["..."]}}` | Validatiefout. |
| 429 | `{"message":"Too Many Attempts."}` | Rate limit overschreden. |
