Appearance
Customer Management
Overview
Customer Management lets you view, create, edit, and delete the customers recorded for your business in the back-office, and drill into a single customer to see their orders, gift cards, reward redemptions, addresses, loyalty balance, marketing preferences, and activity timeline.
Key Purpose: Maintain customer contact details and review their order history, loyalty status, and (when MplusKassa is linked) invoice activity.
Purpose
This page lets you manage per-merchant customer profiles: contact details (name, email, phone, date of birth, language), saved delivery addresses, internal notes, and marketing email/SMS preferences. The customer detail view also surfaces order history, gift cards, reward redemptions, the loyalty point balance, a member code / wallet card, and an activity timeline.
Key Concepts
Customer vs. Customer-Merchant: The system uses a two-level structure. A global
Customerrecord (thecustomermodel in theuserscollection) holds shared contact fields, and a per-merchantCustomerMerchantrecord (thecustomer_vendormodel in the same collection) tracks merchant-specific data such as loyalty points, lifetime visits, total spent, notes, member code, and marketing consent. (app/RawModels/Customer.php,app/RawModels/CustomerMerchant.php)Back-office edits stay merchant-specific: When a back-office user edits a customer, the change is saved only on that merchant's
CustomerMerchantrecord and is not propagated to the globalCustomerrecord. This is intentional for GDPR compliance (app/Services/BackOffice/CustomerService.php:270-271).Contact Uniqueness (per merchant): Before creating or updating, the system checks that no other
CustomerMerchantfor the current merchant already uses the same email or phone; a duplicate returns "Phone or email is already used" (app/Services/BackOffice/CustomerService.php:168-185).Retroactive transaction linking: After creating a customer, the system runs
updateExistingTransactionsCustomerto attach any prior transactions that match the new customer to this record (app/Services/BackOffice/CustomerService.php:227-229).Member code & wallet card: Each customer is assigned a member code on first view, and a digital wallet card can be emailed to them (
app/Services/BackOffice/CustomerService.php:248,CustomerOrchestrator::sendWalletCard).MplusKassa relation (optional): When a customer is linked to an MplusKassa relation, the detail response is enriched with relation data (VAT number, custom pricing, invoice eligibility/block, live points balance), and extra invoice-collection tabs become available (
app/Http/Resources/BackOffice/Customer/ShowCustomerResource.php).Customer self-signup (storefront): Customers also create themselves by signing in on the Online Ordering storefront — either with an email/SMS OTP (
POST /api/customer/send-otp) or with Google sign-in (POST /api/customer/login/google). Both create the globalCustomerrecord when none matches and, when alocation_idis supplied, create the merchantCustomerMerchantrecord and award the loyalty sign-up bonus. Google sign-in also stampsgoogle_idon the customer the first time it is used, and backfills first/last name from the Google token on brand-new accounts only. Both endpoints are unauthenticated and rate-limited to 20 requests per minute per IP. (app/Services/AuthCustomerService.php)Customer self-signup (kiosk) is a different route with different rules. The kiosk never calls
/customer/send-otpor/customer/login/google— it postsPOST /api/kiosk/customer/identify(routes/api.php:207), a device-authenticated route gated by the device profile'sallow_customer_identificationflag and its enabled method list (403 when either is off). There is no OTP and no verification step:member_code/scanonly resolve an existing customer; an unknown code returns 404 "Customer not found."email/phonelook the customer up and create theCustomerplusCustomerMerchantwhen none matches — so anyone can type any address at the kiosk and have an account created for it.
(
AuthCustomerService::identifyCustomerForKiosk)Bot check on the OTP request (Cloudflare Turnstile):
POST /api/customer/send-otpadditionally carries a Turnstile check — it is the call that sends a real SMS/email and grants the loyalty sign-up bonus, and a per-IP rate limit cannot see a flood spread across many IP addresses. The check only enforces once theTURNSTILE_STOREFRONT_SECRET_KEYsetting is filled in; while it is empty the middleware passes every OTP request straight through unverified. Whether it is filled in on production is an environment setting and cannot be read out of the code — check the deployed environment before assuming either way. Nothing on your side to configure — it is a platform-level setting, not a per-merchant one. (routes/api/guest.php:110-111,app/Http/Middleware/VerifyTurnstile.php:31-34.) The kiosk is not affected either way: it identifies customers through its own device-authenticated endpoint (POST /api/kiosk/customer/identify,routes/api.php:207), which has no bot check.ShopCaisse customer sync (optional, pull-only): For locations connected to ShopCaisse, every catalog Sync Now also pulls that store's CRM customers (
GET /v1/stores/{storeId}/customers) and creates/updates a globalCustomerand a merchantCustomerMerchantrecord for each one. This is a one-way ShopCaisse → Upvendo sync; Upvendo never writes customers back to ShopCaisse, and ShopCaisse loyalty balances are not synced (app/Services/Common/ShopCaisseCustomerSyncService.php, invoked fromShopCaisseIntegrationService::sync()).
Actions
All endpoint paths below are relative to /api; the back-office route group mounts them under /api/back-office.
Create a Customer
Add a customer by supplying at least an email or a phone (one is required). The system validates per-merchant uniqueness, creates or re-uses the global Customer record, creates the CustomerMerchant record, and retroactively links matching past transactions. (POST /back-office/customers, CustomerController::store)
Edit Customer Profile
Update first name, last name, email, phone, date of birth, language, or marketing preferences. Uniqueness is re-validated (excluding the customer being edited). The change is saved on the merchant-level record only. (PUT /back-office/customers/{id}, CustomerController::update)
Manage Addresses
Add or edit a customer's delivery address. A new address creates both a global CustomerAddress and a merchant-specific CustomerMerchantAddress; the first address added becomes the default automatically. A separate endpoint sets a different address as default. (POST /back-office/customers/{id}/addresses, POST /back-office/customers/{id}/addresses/{addressId})
Manage Notes
Save a free-text internal note on the customer. (POST /back-office/customers/{id}/notes, CustomerController::storeNotes)
Update Marketing Preferences
Toggle the customer's marketing email and SMS consent independently. (POST /back-office/customers/{id}/marketing, CustomerController::updateMarketingPreferences)
Block Invoice Payments
On the customer detail view, use the Block Invoice payments switch in the right-hand panel. When it is on, this customer cannot place new Invoice orders and no new Stripe invoices can be sent to them. The switch only appears for customers linked to an MplusKassa relation (the whole relation panel is hidden otherwise) and requires the invoice-collection permissions. (PATCH /back-office/customers/{id}/invoice-block, CustomerController::updateInvoiceBlock, permission MANAGE_INVOICE_COLLECTION; a customer with no linked relation returns 422)
View Customer Data
From the detail view, load paginated datatables for orders, gift cards, reward redemptions, and the activity timeline, plus (when MplusKassa is linked) invoice orders and sent/paid invoices. (GET /back-office/customers/{id}/orders, /gift-cards, /reward-redemptions, /timelines, /invoice-orders, /invoice-sent-invoices, /invoice-paid-invoices)
Send Wallet Card
Email the customer their loyalty wallet card (requires an email on file). (POST /back-office/customers/{id}/send-wallet-card)
Export Customers
Export the customer list to an .xlsx spreadsheet with columns Name, Email, Phone, Created At, Points, Visits, Total Spent. Rows are fetched in chunks of 500. (GET /back-office/customers/export, CustomerService::exportToSpreadSheet)
Delete a Customer
Remove the merchant-level customer record. (DELETE /back-office/customers/{id}, CustomerController::delete)
Location
- Backoffice Route:
/marketing/customers(list);/marketing/customers/detail/:id(detail);/marketing/customers/order-detail/:id(single order) - Backend Controller:
app/Http/Controllers/Api/BackOffice/CustomerController.php - Backend Routes:
routes/api/backoffice/customers.php - Backend Service:
app/Services/BackOffice/CustomerService.php - Vue Page (route entry):
src/pages/marketing/customers/index.vue - Vue View:
src/views/marketing/customers/Customers.vue(list),src/views/marketing/customers/Detail.vue(detail) - Create/Edit Form:
src/views/marketing/customers/components/CustomerFormDialog.vue - Nav: under "Marketing" -> "Customers" (
src/navigation/vertical/index.ts:36-40) - Permissions:
VIEW_CUSTOMERS,CREATE_CUSTOMERS,EDIT_CUSTOMERS,DELETE_CUSTOMERS,EXPORT_CUSTOMERS, plusVIEW_INVOICE_COLLECTION(the per-customer Invoice tabs and the Block Invoice payments switch) andMANAGE_INVOICE_COLLECTION(toggling that switch)
Fields
These are the fields accepted by the create/update request (StoreCustomerRequest). Note that not all of them live on the same screen: the CustomerFormDialog.vue create/edit form shows only First Name, Last Name, Language, Email, Phone, and Date of Birth. The two marketing-consent toggles are edited in a separate MarketingDialog.vue (posting to /back-office/customers/{id}/marketing), and Notes are added in a separate AddNoteDialog.vue (posting to /back-office/customers/{id}/notes) — they are not inputs on the main form.
First Name
| Property | Value |
|---|---|
| Field ID | first_name |
| Label | First Name |
| Type | Text |
| Required | No (nullable|string) |
Last Name
| Property | Value |
|---|---|
| Field ID | last_name |
| Label | Last Name |
| Type | Text |
| Required | No (nullable|string) |
Note: There is no separate name field on input. The display name is derived by trimming first_name + last_name.
Email
| Property | Value |
|---|---|
| Field ID | email |
| Label | |
| Type | |
| Required | Conditional (nullable|email) |
| Validation | Valid email; must be unique per merchant |
Description: Customer's email address. Either email or phone is required (phone_or_email becomes required when both are empty).
Phone
| Property | Value |
|---|---|
| Field ID | phone |
| Label | Phone |
| Type | Phone |
| Required | Conditional (nullable|regex:PHONE_REGEX) |
| Validation | Must match Constants::PHONE_REGEX; unique per merchant |
Description: Either email or phone is required.
Date of Birth
| Property | Value |
|---|---|
| Field ID | date_of_birth |
| Label | Date of Birth |
| Type | Composite { day, month, year } (assembled into Y-m-d) |
| Required | No (nullable|date_format:Y-m-d) |
Description: Submitted from the form as separate day/month/year selects, combined to YYYY-MM-DD in prepareForValidation. Used for the loyalty birthday bonus (CustomerMerchant::canClaimBirthdayBonus).
Language
| Property | Value |
|---|---|
| Field ID | language |
| Label | Language |
| Type | Select (options from store.getters.getLanguageOptions) |
| Required | No (nullable|string) |
Receive Marketing Emails
| Property | Value |
|---|---|
| Field ID | receive_marketing_emails |
| Label | Marketing Emails |
| Type | Boolean toggle |
| Required | Yes (required|boolean) |
| Default | false |
Description: Whether the customer consents to marketing emails. This is not a field on the main create/edit form — it is edited in the separate MarketingDialog.vue (posting to /back-office/customers/{id}/marketing). It is still accepted by StoreCustomerRequest (defaulted to false server-side when the form omits it).
Receive Marketing SMS
| Property | Value |
|---|---|
| Field ID | receive_marketing_sms |
| Label | Marketing SMS |
| Type | Boolean toggle |
| Required | Yes (required|boolean) |
| Default | false |
Description: Whether the customer consents to marketing SMS. Like marketing emails, this is edited in the separate MarketingDialog.vue (posting to /back-office/customers/{id}/marketing), not on the main create/edit form.
Location ID
| Property | Value |
|---|---|
| Field ID | location_id |
| Label | Location |
| Type | String |
| Required | No (nullable|string) |
Note: Accepted by StoreCustomerRequest, but the back-office store flow unsets location_id before saving (it is only used by guided setup).
Notes
| Property | Value |
|---|---|
| Field ID | notes |
| Label | Internal Notes |
| Type | Textarea |
| Required | Yes when saved via the notes endpoint (required|string) |
Description: Internal note about the customer (not customer-facing). Added via a separate AddNoteDialog.vue (posting to POST /back-office/customers/{id}/notes), not via the main create/edit form.
Address sub-fields (StoreCustomerAddressRequest)
| Field ID | Type | Required |
|---|---|---|
id | String (when editing an existing address) | No |
country | String | Yes |
first_name | String | No |
last_name | String | No |
company | String | No |
address | String | Yes |
unit_number | String | No |
postal_code | String | No |
city | String | No |
state | String | No |
phone | String (regex:PHONE_REGEX) | No |
Read-only / derived fields (detail view)
These appear in the customer detail response (ShowCustomerResource) but are not editable inputs:
name-- trimmed first + last namemember_code-- assigned member/wallet codeaddresses,default_address-- saved addressesreadable_dob-- formatted date of birthamount_spent-- sum of completed transactionsorder_count-- number of transactionslast_seen-- date of latest transactionloyalty.current_points,loyalty.lifetime_pointsmplus-- MplusKassa relation data (only when linked)
Business Logic
Customer Creation (back-office)
Back-office user submits email and/or phone
│
▼
Validate uniqueness per merchant (email/phone)
│
▼
Find or create global Customer record (matched by email/phone)
│
▼
Create CustomerMerchant record (vendor_id + customer_id)
│
▼
Retroactively link matching existing transactionsCustomer Self-Signup (storefront / kiosk)
Customer signs in on the storefront or kiosk
├── Email/SMS OTP → POST /api/customer/send-otp
└── Google sign-in → POST /api/customer/login/google
│ (both unauthenticated, throttled 20 req/min per IP;
│ send-otp also carries a Cloudflare Turnstile bot check,
│ inert until TURNSTILE_STOREFRONT_SECRET_KEY is set)
▼
Global Customer matched by email (Google) or by the chosen
email/phone (OTP)?
├── Yes → re-use it
└── No → create the global Customer, backfilling first/last
name from the Google token (Google path only)
│
▼
Google path: stamp google_id if the customer has none yet
│
▼
location_id supplied?
├── Yes → create/get the CustomerMerchant for that location's merchant,
│ and award the loyalty sign-up bonus if the customer was new
└── No → global Customer only; no merchant record yet(app/Services/AuthCustomerService.php)
Address Default
Add address
│
▼
Customer has no default address?
├── Yes → newly added address becomes default
└── No → default unchanged (use "set default" endpoint to change)ShopCaisse Customer Sync (per location, optional)
Backoffice "Sync Now" on a ShopCaisse-connected location
│
▼
SyncShopCaisseCatalogJob → ShopCaisseIntegrationService::sync()
│ (after catalog/modifier/menu/seating-plan/stock sync,
│ only when a ShopCaisse store id resolved for the location)
▼
ShopCaisseService::syncCustomers() fetches store.customerGroup and
paginated GET /v1/stores/{storeId}/customers
│
▼
For each ShopCaisse customer record with an id AND (email OR phone):
│
▼
Match existing CustomerMerchant, in order:
1. external_data.shopcaisse.customer_ids contains this id
2. lowercase email match within this merchant
3. phone match within this merchant (only if ShopCaisse record has no email)
│
├── Match found → update non-empty first/last name, email, phone,
│ date_of_birth, company on the CustomerMerchant
│ (empty ShopCaisse values never blank existing data);
│ loyalty points, visits, spend, notes, marketing
│ consent, and member code are left untouched
│
└── No match → find/create the global Customer (matched by email,
else phone) filling only blank global fields, then
create a new CustomerMerchant record
│
▼
Record ShopCaisse provenance on CustomerMerchant.external_data.shopcaisse
(customer_ids, per-store customer/customer-group id, last_synced_at) —
this is internal bookkeeping only; it is not shown as a tab in the
customer detail UI (unlike the MplusKassa relation)Records marked deleted: true or active: false upstream are skipped, as are records with no id or no email/phone. A ShopCaisse record missing later from a sync is not deleted or archived in Upvendo. (app/Services/Common/ShopCaisseCustomerSyncService.php)
Relations
Depends On
- Loyalty Program: For the points balance shown in the detail view
- MplusKassa integration (optional): For invoice tabs and live POS points balance
- ShopCaisse integration (optional): For locations connected to ShopCaisse, each catalog "Sync Now" also pulls that store's CRM customers and creates/updates global
Customerand merchantCustomerMerchantrecords (pull-only; no UI change, just populated/refreshed contact fields)
Affects
- Transactions: Orders are linked to the customer-merchant record
- Loyalty: Points earned/redeemed per customer-merchant
- Marketing: Email/SMS targeting
Related Features
Business Rules
- Either an email or a phone is required to create a customer; if both are empty, validation fails on
phone_or_email(StoreCustomerRequest::rules). - Email and phone must be unique per merchant. Creating or updating with a duplicate returns a 400 "Phone or email is already used" (
CustomerService::isCustomerValid). - Back-office edits are saved on the merchant-level record only and are not synced back to the global customer record (GDPR-driven decision,
CustomerService::updatecomment, lines 270-271). - The first address added to a customer is automatically set as the default; later additions only change the default via the explicit "set default" endpoint.
- Marketing email and SMS consent are tracked as two independent booleans on the merchant-level record (not a single opt-in flag).
- Export aborts with "No customers found" when the first page has zero results; rows are otherwise fetched 500 at a time until a short page is returned.
- ShopCaisse customer sync only runs for a location if a ShopCaisse store id resolved during that sync; it fetches, never pushes — Upvendo never writes customers back to ShopCaisse and ShopCaisse loyalty balances are never synced.
- ShopCaisse-sourced updates on the merchant record only overwrite a field when ShopCaisse supplies a non-empty value; they never blank an existing Upvendo value, and they never touch loyalty points, visits, spend, notes, marketing consent, or member code.
- On the shared global
Customerrecord, ShopCaisse can only fill in currently-blank fields (name, email, phone, date of birth) — it cannot overwrite an already-set cross-merchant identity field. - Customer sign-in from the storefront (
POST /api/customer/send-otp) is bot-checked with Cloudflare Turnstile, but the check only enforces when the platform settingTURNSTILE_STOREFRONT_SECRET_KEYis configured. While it is empty the request is let through unverified — an unconfigured setting disables the protection, it never locks customers out (app/Http/Middleware/VerifyTurnstile.php:31-34). - When the check is enforcing, a sign-in request that carries no
cf-turnstile-responsetoken is refused with HTTP 403CAPTCHA_REQUIRED/ "Verification required", and one with a token Cloudflare rejects (or that could not be checked, e.g. Cloudflare unreachable) is refused with 403CAPTCHA_INVALID/ "Verification failed" (VerifyTurnstile.php:36-73). POST /api/customer/login(submitting the OTP code) andPOST /api/customer/login/googlehave no Turnstile check — only the send-OTP step does (routes/api/guest.php:112-113).
FAQs
- "What happens when a customer is created from the back-office?" The system validates email/phone uniqueness per merchant, finds or creates the global customer record, creates the merchant-specific record, then links any existing transactions that match the contact info.
- "Can a customer belong to multiple merchants?" Yes. The global
Customerrecord is shared, and a separateCustomerMerchantrecord is created per merchant, each tracking its own points, visits, notes, and marketing consent. - "Do edits in the back-office update the customer's global profile?" No. Back-office edits are deliberately scoped to the current merchant's record only (for GDPR compliance) and are not propagated globally.
- "How does marketing opt-in work?" Marketing email and SMS are two separate boolean preferences stored on the merchant-level record. They are edited in the separate Marketing dialog on the customer detail view (
POST /back-office/customers/{id}/marketing) — not on the create/edit form, which only has name, language, email, phone and date of birth. - "Where do the customers in my list come from?" Three sources: you create them in the back office, they sign themselves in on your storefront or kiosk (email/SMS OTP or Google sign-in), or ShopCaisse syncs them in.
- "Is there anything stopping bots from signing up on my storefront?" Two things. Every request to the customer sign-in endpoints is rate-limited to 20 per minute per IP address, and the send-OTP step additionally has a Cloudflare Turnstile bot check. The Turnstile check is a platform-level setting that only enforces while its secret is configured — if the secret is empty, sign-in requests pass through unverified and a storefront that sends no token can still log a customer in. Whether the secret is set on production is an environment question, not something the code can answer.
- "How does the address default work?" The first address added becomes the default automatically; to change it, use the set-default address action.
- "Where do invoice tabs come from?" They appear only when the customer is linked to an MplusKassa relation and invoice collection is enabled for the merchant.
- "Does connecting ShopCaisse add customers automatically?" Yes. For a location connected to ShopCaisse, every catalog Sync Now on that location also pulls the store's CRM customers and creates or updates the matching Upvendo customer (global + merchant records). This runs automatically as part of the existing ShopCaisse sync — there is no separate customer-only sync button.
- "Why did a customer's name/email/phone change after a ShopCaisse sync?" ShopCaisse is treated as authoritative for merchant-level contact fields (first/last name, email, phone, date of birth, company) whenever it supplies a non-empty value for a customer matched to that store; loyalty points, visits, spend, notes, marketing consent, and member code are never touched by the sync.
- "Can I push an Upvendo customer to ShopCaisse?" No. The ShopCaisse customer sync is pull-only (ShopCaisse → Upvendo); Upvendo never writes customer data back to ShopCaisse, and ShopCaisse loyalty balances are not synced.
- "A ShopCaisse customer disappeared from a later sync — was it deleted in Upvendo?" No. A customer missing from a later ShopCaisse pull is left as-is; Upvendo does not delete or archive customers based on ShopCaisse sync results.
Troubleshooting
Problem: "Phone or email is already used" when saving
Causes:
- Another customer for this merchant already uses that email or phone
Solutions:
- Search for the existing customer and edit that record instead
- Use a different email/phone
Problem: "Email or phone already exists" when saving
Cause: This is a different error from the one above. The email or phone matches more than one global customer record (the record shared across merchants), so the system cannot decide which global profile to attach the new merchant record to. (CustomerService::retrieveParentCustomer)
Solution: This is a data-integrity condition on the shared customer record, not a per-merchant duplicate — changing the value on your own customer will not clear it. Contact support to merge the duplicate global records.
Problem: Cannot save the customer at all
Causes:
- Both email and phone left blank (
phone_or_emailrequired) - Phone does not match the expected format (
PHONE_REGEX)
Solutions:
- Provide at least an email or a phone
- Enter the phone in a valid format
Problem: "Send wallet card" fails
Causes:
- Customer has no email address on file
Solutions:
- Add an email to the customer, then resend
Problem: Invoice tabs not showing on the customer detail
Causes:
- Customer is not linked to an MplusKassa relation, or
- Invoice collection is not enabled for the merchant
Solutions:
- Link the customer to the correct MplusKassa relation
- Enable invoice collection for the merchant
Problem: Customers cannot sign in on the storefront — "Verification required" / "Verification failed" (403)
Causes:
- The Cloudflare Turnstile bot check on
POST /api/customer/send-otphas been switched on (TURNSTILE_STOREFRONT_SECRET_KEYconfigured) but the storefront build does not send acf-turnstile-responsetoken → 403CAPTCHA_REQUIRED, "Verification required" on every sign-in attempt - The token was sent but Cloudflare rejected it, or Cloudflare could not be reached within the 5-second timeout → 403
CAPTCHA_INVALID, "Verification failed" (app/Http/Middleware/VerifyTurnstile.php:45-73)
Solutions:
- This is a platform-level configuration issue, not a merchant setting — contact support. Clearing the setting restores sign-in immediately, because an unset secret makes the check pass everything through
- Confirm the storefront release that renders the Turnstile widget is deployed before the setting is enabled
Problem: ShopCaisse customers not appearing in the customer list
Causes:
- The location is not connected to ShopCaisse, or no ShopCaisse store id resolved for the location
- "Sync Now" has not been run since the customer was added in ShopCaisse
- The ShopCaisse record has neither an id nor an email/phone (skipped by the sync), or is marked
deleted/active: falseupstream
Solutions:
- Verify the ShopCaisse integration status for the location (
/shopcaisseStatus tab) - Click "Sync Now" to re-run the catalog + customer sync
- Fix the customer record in ShopCaisse so it has an id plus a valid email or phone
Assistant Guidance
When answering questions about this feature:
- Distinguish the three customer-source flows: back-office manual create/edit (merchant-scoped only, never syncs to the global record); customer self-signup on the storefront/kiosk (OTP or Google sign-in, which creates the customer and awards the loyalty sign-up bonus when a location is in play); and the ShopCaisse pull sync (runs automatically inside "Sync Now" on a ShopCaisse-connected location, updates merchant contact fields but never loyalty/notes/marketing consent).
- ShopCaisse customer sync has no dedicated UI — it is not a toggle or a separate button. It happens as a side effect of the existing ShopCaisse "Sync Now" action, and its data does not add a tab to the customer detail view the way an MplusKassa relation does.
- If a merchant asks why a customer's contact info changed unexpectedly and they use ShopCaisse, check whether a sync ran recently before assuming a manual edit.
- Don't imply MplusKassa is the only POS integration that touches customer records — ShopCaisse also creates/updates customers, but only via the pull-only CRM sync (no invoice tabs, no live points balance).
Examples
Create customer payload (POST /back-office/customers)
json
{
"first_name": "Jan",
"last_name": "de Vries",
"email": "jan@example.com",
"phone": "+31612345678",
"language": "nl",
"date_of_birth": { "day": "15", "month": "03", "year": "1985" },
"receive_marketing_emails": true,
"receive_marketing_sms": false
}Add address payload (POST /back-office/customers/{id}/addresses)
json
{
"country": "NL",
"first_name": "Jan",
"last_name": "de Vries",
"company": "",
"address": "Kalverstraat 123",
"unit_number": "4",
"postal_code": "1012 AB",
"city": "Amsterdam",
"state": ""
}Customer detail response (GET /back-office/customers/{id}, abbreviated)
json
{
"id": "65f...",
"first_name": "Jan",
"last_name": "de Vries",
"name": "Jan de Vries",
"member_code": "12345678",
"email": "jan@example.com",
"phone": "+31612345678",
"readable_dob": "March 15, 1985",
"date_of_birth": { "day": "15", "month": "03", "year": "1985" },
"amount_spent": "€567.50",
"order_count": 23,
"last_seen": "Jan 10, 2024",
"loyalty": { "current_points": 450, "lifetime_points": 1200 },
"notes": "Prefers oat milk",
"language": "nl",
"receive_marketing_emails": true,
"receive_marketing_sms": false,
"mplus": null
}