Skip to content

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 Customer record (the customer model in the users collection) holds shared contact fields, and a per-merchant CustomerMerchant record (the customer_vendor model 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 CustomerMerchant record and is not propagated to the global Customer record. 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 CustomerMerchant for 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 updateExistingTransactionsCustomer to 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 global Customer record when none matches and, when a location_id is supplied, create the merchant CustomerMerchant record and award the loyalty sign-up bonus. Google sign-in also stamps google_id on 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-otp or /customer/login/google — it posts POST /api/kiosk/customer/identify (routes/api.php:207), a device-authenticated route gated by the device profile's allow_customer_identification flag and its enabled method list (403 when either is off). There is no OTP and no verification step:

    • member_code / scan only resolve an existing customer; an unknown code returns 404 "Customer not found."
    • email / phone look the customer up and create the Customer plus CustomerMerchant when 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-otp additionally 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 the TURNSTILE_STOREFRONT_SECRET_KEY setting 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 global Customer and a merchant CustomerMerchant record 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 from ShopCaisseIntegrationService::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, plus VIEW_INVOICE_COLLECTION (the per-customer Invoice tabs and the Block Invoice payments switch) and MANAGE_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

PropertyValue
Field IDfirst_name
LabelFirst Name
TypeText
RequiredNo (nullable|string)

Last Name

PropertyValue
Field IDlast_name
LabelLast Name
TypeText
RequiredNo (nullable|string)

Note: There is no separate name field on input. The display name is derived by trimming first_name + last_name.


Email

PropertyValue
Field IDemail
LabelEmail
TypeEmail
RequiredConditional (nullable|email)
ValidationValid 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

PropertyValue
Field IDphone
LabelPhone
TypePhone
RequiredConditional (nullable|regex:PHONE_REGEX)
ValidationMust match Constants::PHONE_REGEX; unique per merchant

Description: Either email or phone is required.


Date of Birth

PropertyValue
Field IDdate_of_birth
LabelDate of Birth
TypeComposite { day, month, year } (assembled into Y-m-d)
RequiredNo (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

PropertyValue
Field IDlanguage
LabelLanguage
TypeSelect (options from store.getters.getLanguageOptions)
RequiredNo (nullable|string)

Receive Marketing Emails

PropertyValue
Field IDreceive_marketing_emails
LabelMarketing Emails
TypeBoolean toggle
RequiredYes (required|boolean)
Defaultfalse

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

PropertyValue
Field IDreceive_marketing_sms
LabelMarketing SMS
TypeBoolean toggle
RequiredYes (required|boolean)
Defaultfalse

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

PropertyValue
Field IDlocation_id
LabelLocation
TypeString
RequiredNo (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

PropertyValue
Field IDnotes
LabelInternal Notes
TypeTextarea
RequiredYes 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 IDTypeRequired
idString (when editing an existing address)No
countryStringYes
first_nameStringNo
last_nameStringNo
companyStringNo
addressStringYes
unit_numberStringNo
postal_codeStringNo
cityStringNo
stateStringNo
phoneString (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 name
  • member_code -- assigned member/wallet code
  • addresses, default_address -- saved addresses
  • readable_dob -- formatted date of birth
  • amount_spent -- sum of completed transactions
  • order_count -- number of transactions
  • last_seen -- date of latest transaction
  • loyalty.current_points, loyalty.lifetime_points
  • mplus -- 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 transactions

Customer 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 Customer and merchant CustomerMerchant records (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

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::update comment, 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 Customer record, 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 setting TURNSTILE_STOREFRONT_SECRET_KEY is 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-response token is refused with HTTP 403 CAPTCHA_REQUIRED / "Verification required", and one with a token Cloudflare rejects (or that could not be checked, e.g. Cloudflare unreachable) is refused with 403 CAPTCHA_INVALID / "Verification failed" (VerifyTurnstile.php:36-73).
  • POST /api/customer/login (submitting the OTP code) and POST /api/customer/login/google have 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 Customer record is shared, and a separate CustomerMerchant record 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:

  1. Another customer for this merchant already uses that email or phone

Solutions:

  1. Search for the existing customer and edit that record instead
  2. 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:

  1. Both email and phone left blank (phone_or_email required)
  2. Phone does not match the expected format (PHONE_REGEX)

Solutions:

  1. Provide at least an email or a phone
  2. Enter the phone in a valid format

Problem: "Send wallet card" fails

Causes:

  1. Customer has no email address on file

Solutions:

  1. Add an email to the customer, then resend

Problem: Invoice tabs not showing on the customer detail

Causes:

  1. Customer is not linked to an MplusKassa relation, or
  2. Invoice collection is not enabled for the merchant

Solutions:

  1. Link the customer to the correct MplusKassa relation
  2. Enable invoice collection for the merchant

Problem: Customers cannot sign in on the storefront — "Verification required" / "Verification failed" (403)

Causes:

  1. The Cloudflare Turnstile bot check on POST /api/customer/send-otp has been switched on (TURNSTILE_STOREFRONT_SECRET_KEY configured) but the storefront build does not send a cf-turnstile-response token → 403 CAPTCHA_REQUIRED, "Verification required" on every sign-in attempt
  2. 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:

  1. 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
  2. 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:

  1. The location is not connected to ShopCaisse, or no ShopCaisse store id resolved for the location
  2. "Sync Now" has not been run since the customer was added in ShopCaisse
  3. The ShopCaisse record has neither an id nor an email/phone (skipped by the sync), or is marked deleted/active: false upstream

Solutions:

  1. Verify the ShopCaisse integration status for the location (/shopcaisse Status tab)
  2. Click "Sync Now" to re-run the catalog + customer sync
  3. 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
}