borkoldocs

Contacts

Contact accounts

How storefront contact accounts, session tokens, and the guest lifecycle work.

View as Markdown

Contacts give your storefront contact accounts: registration, login, password reset, a profile, and saved addresses. All routes live under /v1/api/contacts, with an identical alias at /v1/api/customers for integrations built before the rename. Contacts are part of every store, so there is no module to enable first.

Alias: /v1/api/customers

Every route below is also served, unchanged, at the equivalent /v1/api/customers/... path (for example POST /v1/api/customers/register alongside POST /v1/api/contacts/register). Request and response shapes are identical on both. This alias is permanent, not a deprecation window: existing integrations never need to migrate, though new integrations should use /v1/api/contacts.

Two tokens, two headers

Contact endpoints use a layered authentication model:

  1. Tenant API key (always required). Every request carries your store's API key as Authorization: Bearer {api_key}, exactly like the rest of the storefront API. The key needs the contacts:write scope for all POST, PATCH, and DELETE endpoints and the contacts:read scope for the GET endpoints. A key minted before the rename carries customers:write / customers:read instead, and is still accepted. A missing scope returns 403 {"message":"Invalid ability provided."}.
  2. Contact session token (required on contact-authenticated endpoints). POST /v1/api/contacts/register and POST /v1/api/contacts/login return a token string. Send it on subsequent requests in the X-Customer-Token header. A missing or invalid contact token returns 401 {"message":"Invalid customer token."}.

The two headers are independent and both required on contact-authenticated routes:

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

Which endpoints need which:

EndpointAPI key scopeX-Customer-Token
POST /registercontacts:write (customers:write accepted)No
POST /logincontacts:write (customers:write accepted)No
POST /forgot-passwordcontacts:write (customers:write accepted)No
POST /reset-passwordcontacts:write (customers:write accepted)No
POST /logoutcontacts:write (customers:write accepted)Yes
GET /mecontacts:read (customers:read accepted)Yes
PATCH /mecontacts:write (customers:write accepted)Yes
GET /me/addressescontacts:read (customers:read accepted)Yes
POST /me/addressescontacts:write (customers:write accepted)Yes
PATCH /me/addresses/{id}contacts:write (customers:write accepted)Yes
DELETE /me/addresses/{id}contacts:write (customers:write accepted)Yes

Contact session tokens do not expire on a timer. They are revoked by POST /logout (one token) and by a successful password reset (all of the contact's tokens).

The public contact object

Contact-facing endpoints return this shape (internal fields such as is_guest, type, and customer_group_id are never exposed on the storefront API):

FieldTypeDescription
idstring (uuid)Contact id. Stable across the guest-to-account upgrade.
namestringFull name.
emailstring or nullEmail address, always lowercase when present. Unique per store. Registering through the storefront always requires an email; a contact can only have a null email when it was created by the merchant (for example a company contact added through the client dashboard or CSV import) without one.
phonestring or nullPhone number.
kindstringperson or company. Read-only through this API: set by the merchant, not by the storefront.
company_namestring or nullCompany name. Set only when kind is company. Read-only through this API.
registration_numberstring or nullCompany registration number (for example a Dutch KvK number). Read-only through this API.
vat_idstring or nullVAT id. Read-only through this API.
external_idstring or nullMerchant-assigned identifier, for example an ERP or accounting system id. Read-only through this API.
display_namestringThe company name when there is one, the contact's name otherwise. Derived, not stored.
created_atstring (ISO 8601)Creation timestamp, for example 2026-08-14T10:32:11+00:00.

kind, company_name, registration_number, vat_id, and external_id are readable on every contact but not writable through register or PATCH /me: a storefront account cannot turn itself into a company contact. The merchant sets these fields from the client dashboard's Contacts pages.

Guest lifecycle

Cart email capture in the Commerce module can create a contact row with only an email and no password (a guest). This matters in three places:

  • Registration upgrades guests. Registering with an email that belongs to a guest row upgrades that row in place: same id, order history preserved. Only an email that belongs to a real (non-guest) account fails with 422 "Email already registered.".
  • Guests cannot log in. They have no password, so login fails with the generic 422 "Invalid credentials.".
  • Password reset also upgrades. Completing a reset sets a password, marks the account non-guest, and sets email_verified_at if it was unset. This is a second path from guest checkout to a full account.

Rate limits

All contact endpoints share the storefront limit of 120 requests per minute per store (429 with a Retry-After header when exceeded). The four credential endpoints (register, login, forgot-password, reset-password) carry an additional limit of 10 requests per minute per client IP to slow brute force attempts.

Errors

All error envelopes are the standard storefront shapes:

StatusBodyCause
401{"message":"Unauthenticated."}Missing or invalid tenant API key.
401{"message":"Invalid customer token."}Missing or invalid X-Customer-Token on a contact-authenticated route.
403{"message":"Invalid ability provided."}API key lacks the required scope.
403{"message":"Tenant suspended.","code":"tenant_suspended"}Store suspended.
404{"message":"..."}Resource not found (or not owned by the authenticated contact).
422{"message":"...","errors":{"field":["..."]}}Validation failure.
429{"message":"Too Many Attempts."}Rate limit exceeded.