# Registration and sessions

These three endpoints manage contact accounts and session tokens. `register` and `login` return a `token` alongside the contact; send it as `X-Customer-Token` on contact-authenticated endpoints. All three require the `contacts:write` scope on your API key (`customers:write` accepted).

## POST /v1/api/contacts/register

Also served at `/v1/api/customers/register` for existing integrations; identical request and response.

Register a storefront contact account. Returns the created contact plus a session token. If the email already belongs to a guest row (created by cart email capture in the Commerce module), the guest is upgraded in place: same `id`, order history preserved, name and password set from this request. Only an email that belongs to a real (non-guest) account is rejected. Extra rate limit: 10 requests per minute per IP.

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Full name. Maximum 150 characters. |
| `email` | string | Yes | Valid email address, maximum 255 characters. Lowercased server-side. Must not belong to an existing non-guest account. |
| `password` | string | Yes | Minimum 8 characters. |

```bash title="Request"
curl -X POST https://api.borkol.com/v1/api/contacts/register \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"name":"Jane Doe","email":"jane@example.com","password":"s3cret-pass"}'
```

```javascript title="Request (JS)"
const res = await fetch("https://api.borkol.com/v1/api/contacts/register", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Jane Doe",
    email: "jane@example.com",
    password: "s3cret-pass",
  }),
});
const { data: contact, token } = await res.json();
```

Status: `201 Created`.

```json title="Response"
{
  "data": {
    "id": "9d4e2f1a-6b3c-4f8e-9a1d-2c5b7e8f0a3d",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": null,
    "kind": "person",
    "company_name": null,
    "registration_number": null,
    "vat_id": null,
    "external_id": null,
    "display_name": "Jane Doe",
    "created_at": "2026-08-14T10:32:11+00:00"
  },
  "token": "12|Fq8sT3vY6wZx9aBcDeFgHiJkLmNoPqRsTuVwXyZ0"
}
```

### Errors

- `422` with `errors.email` `["Email already registered."]` when a non-guest account already uses the email.
- `422` for field validation failures (missing name, invalid email, password shorter than 8 characters).
- `429` when the 10 requests per minute per IP limit is exceeded.

## POST /v1/api/contacts/login

Also served at `/v1/api/customers/login` for existing integrations; identical request and response.

Authenticate a contact with email and password. Returns the contact and a fresh session token, and updates the account's `last_login_at`. The check is timing-safe and never reveals whether the email exists: a wrong password, an unknown email, and a guest account (which has no password) all fail with the same 422. Extra rate limit: 10 requests per minute per IP.

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | Valid email address. Lowercased server-side before lookup. |
| `password` | string | Yes | The account password. |

```bash title="Request"
curl -X POST https://api.borkol.com/v1/api/contacts/login \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com","password":"s3cret-pass"}'
```

Status: `200 OK`.

```json title="Response"
{
  "data": {
    "id": "9d4e2f1a-6b3c-4f8e-9a1d-2c5b7e8f0a3d",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": "+31612345678",
    "kind": "person",
    "company_name": null,
    "registration_number": null,
    "vat_id": null,
    "external_id": null,
    "display_name": "Jane Doe",
    "created_at": "2026-08-14T10:32:11+00:00"
  },
  "token": "13|Gh7rS2uX5vWy8zAbCdEfGhIjKlMnOpQrStUvWxYz"
}
```

### Errors

- `422` with `errors.email` `["Invalid credentials."]` for a wrong email or a wrong password. The error is always on the `email` key.
- `429` when the 10 requests per minute per IP limit is exceeded.

## POST /v1/api/contacts/logout

Also served at `/v1/api/customers/logout` for existing integrations; identical request and response.

Revoke the contact session token given in `X-Customer-Token`. The header must be present and valid to pass contact authentication: a currently valid token is revoked and the request returns 204, while an already-revoked or unknown token fails authentication with 401 `{"message": "Invalid customer token."}`.

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `X-Customer-Token` | string | Yes | The session token to revoke. |

No body.

```bash title="Request"
curl -X POST https://api.borkol.com/v1/api/contacts/logout \
  -H "Authorization: Bearer {api_key}" \
  -H "X-Customer-Token: 13|Gh7rS2uX5vWy8zAbCdEfGhIjKlMnOpQrStUvWxYz"
```

Status: `204 No Content`. The response has no body.

### Errors

- `401` `{"message":"Invalid customer token."}` when `X-Customer-Token` is missing or invalid.
