OpenShopy
Dokumentacja API

Customers & Customer Accounts

Two distinct concepts:

  1. Merchant-managed customers (customers:read/customers:write, secret key) — CRM-style records.
  2. Customer accounts (customer_accounts scope, usable with a publishable key) — storefront self-service login, used by end-shoppers. Authenticated via a bearer-like session token sent as X-Customer-Token (separate from the API key).

A customers row may or may not have a password_hash; the API exposes this as has_account: boolean and strips password_hash from all responses.

Merchant-managed customers

GET /customers

Scope: customers:read, secret required.

Query params:

ParamTypeNotes
page,limitintpagination
qstringsearches email/first_name/last_name/company (ILIKE)
emailstringILIKE exact
tagstringcontains tag
accepts_marketing"true"/otherboolean filter
min_ordersnumberorders_count >=
min_spentnumbertotal_spent >=

Returns array of customer objects + pagination meta.

GET /customers/:id

Scope: customers:read, secret required. Returns customer plus addresses: CustomerAddress[].

POST /customers

Scope: customers:write, secret required.

Body:

FieldTypeRequiredConstraints
emailstringyesvalid email; stored lowercased
first_namestring|nullnomax 100
last_namestring|nullnomax 100
phonestring|nullnomax 40
companystring|nullnomax 200
vat_idstring|nullnomax 30
languageenumnopl|en, default pl
accepts_marketingboolno
tagsstring[]nomax 30, each max 60
notestring|nullnomax 5000

409 duplicate (with details.id) if a customer with that email already exists in the store. Fires customer.created automation. Response 201.

PATCH /customers/:id

Scope: customers:write, secret required. Same fields, all optional/partial. Email re-lowercased if present. 422 update_failed on DB error.

DELETE /customers/:id

Scope: customers:write, secret required. Hard deletes. Returns { "id", "deleted": true }.

GET /customers/:id/orders

Scope: orders:read, secret required. Paginated order history for this customer (secret serialization).

POST /customers/:id/addresses

Scope: customers:write, secret required. Body: address schema + is_default?: bool. Requires line1, city, postal_code (else 422 invalid_address). If is_default, clears default flag on other addresses first. country_code defaults to "PL" if omitted. Response 201 with the address row.

DELETE /customers/:id/addresses/:addressId

Scope: customers:write, secret required. Returns { "deleted": true } (no existence check — always "succeeds").

Customer accounts (storefront self-service)

Session tokens: format cst_<random>, stored hashed, valid 30 days, sent back by the client as header X-Customer-Token: <token> on all /customer-auth/me* routes. There's no refresh mechanism; client re-logs in after expiry.

POST /customer-auth/register

Scope: customer_accounts.

Body: { email: string(email), password: string(8-200), first_name?: string(max100)|null, last_name?: string(max100)|null, phone?: string(max40)|null, accepts_marketing?: bool, language?: "pl"|"en" }.

If a customer record with that email exists but has no password yet (e.g. created via an order), it's "claimed" (password attached, missing fields filled in). If it already has a password: 409 account_exists. New/claimed customer triggers customer_welcome notification and (for brand-new customers) customer.created automation. Response 201:

json
{
  "data": {
    "customer": { "...": "serialized customer (has_account:true)" },
    "session": { "token": "cst_xxx...", "expires_at": "2024-06-01T00:00:00.000Z" }
  }
}

POST /customer-auth/login

Scope: customer_accounts. Body: { email, password }. 401 invalid_credentials on mismatch or missing password hash. Response: { customer, session }.

POST /customer-auth/logout

Scope: customer_accounts. Reads X-Customer-Token header (optional) and deletes that session. Always returns { "ok": true }.

POST /customer-auth/password-reset

Scope: customer_accounts. Body: { email: string(email), reset_url?: string(url) }. Always returns { "ok": true } regardless of whether the email exists (no enumeration). If it exists, creates a reset token (1 hour expiry) and sends password_reset notification with reset_url variable = <reset_url or store.storefront_url or origin>/reset-password?token=<token> (uses & if the base already has a ?).

POST /customer-auth/password-reset/confirm

Scope: customer_accounts. Body: { token: string(min 20), password: string(8-200) }. 400 invalid_token if token missing/used/expired. On success: updates password hash, marks reset used, invalidates all existing sessions for that customer, and returns a fresh session: { "ok": true, "session": {...} }.

GET /customer-auth/me

Scope: customer_accounts. Requires X-Customer-Token. Returns the logged-in customer (note/tags explicitly stripped/undefined in this response) + addresses.

PATCH /customer-auth/me

Scope: customer_accounts. Body (all optional): first_name, last_name, phone, company, vat_id, accepts_marketing, language, plus optional password change via current_password + new_password (min 8). 401 invalid_credentials if current_password doesn't match when changing password. Returns updated customer.

POST /customer-auth/me/addresses

Scope: customer_accounts. Same address rules as the merchant endpoint. Response 201.

DELETE /customer-auth/me/addresses/:addressId

Scope: customer_accounts. Deletes only if it belongs to the authenticated customer.

GET /customer-auth/me/orders

Scope: customer_accounts. Paginated order history (non-secret serialization) for the logged-in customer.

GET /customer-auth/me/orders/:id

Scope: customer_accounts. :id may be a UUID or a numeric order number. 404 not_found if it doesn't belong to this customer.

Public order tracking (no customer account needed)

GET /orders/track

See Orders — no scope needed (any valid key), query params number + email.