ClientAccountModule serves /client-data/* (63 routes): everything a "My account" page needs, always for the person on the request. Customer-facing sites and apps call it; the admin side uses the CRM and storefront modules instead.
Who "me" is
No route takes a customer id. The person is resolved from the request:
x-client-authorization: Bearer <token>— the customer's session, sent next to the site's app token. It must be a customer or user token of this org, or the request is refused401.- With no client header, the bearer token itself — unless it is a
Systemidentity (the site's app token), which is no one. - An expired client token is treated as signed out, not as the app: routes answer as if nobody is signed in rather than returning the integration account's empty data.
So a request with only the app token reaches the controller but has no "me". dashboard and profile then return null; files, all, forms, reservations, tickets, profile update and benefit enrollment answer 401; one order by id answers 403. Send the client header on every call. See customer authentication and endpoints.
How it differs from /repository
/repository is generic CRUD on any datatype, governed by permissions and whatever query the caller sends. /client-data decides the owner itself — orders and addresses by the session's author, forms by its email, wishlists by any of its identifiers, files by a folder named for it — and returns shaped views (a profile patch limited to three fields, a form row that hides its access token). Nothing in the body or path can widen the scope to another customer.
Guards
Every route is jwt except one @PublicRoute (invitation validation). There is no @StaffOnly or @Roles here; the class-level @Roles(Customer) is commented out. Rules are enforced per route in the service: shared-account routes need a manager, PUT profile needs a customer (a staff user gets 403), wishlist ids must belong to the caller.
Dashboard
/client-data/dashboardJWT/client-data/allJWTdashboard bundles profile, addresses, recent orders, reservations, open tickets, notifications, analytics, benefits and affiliate stats with a summary. all returns every section for an account page; each section stands alone — one the caller may not read (for example tickets answering 403) comes back empty instead of failing the page.
Profile and verification
/client-data/profileJWT/client-data/profileJWT/client-data/verification/statusJWT/client-data/verification/email/sendJWT/client-data/verification/email/verifyJWT/client-data/verification/phone/sendJWT/client-data/verification/phone/verifyJWTPUT profile accepts only firstName, lastName and phone (1–120 characters; phone 7–15 digits, empty removes it). Anything else is 400. Changing the phone resets phoneVerified.
Orders, forms and transactions
/client-data/ordersJWT/client-data/orders/:orderIdJWT/client-data/formsJWT/client-data/transactionsJWTOrders are those authored by the session's email, username or id. forms returns the customer's form_submission rows matched on their email — ?status=, ?page=, ?limit= (max 100) — including pending rows (a form link asked for and not yet used), each with the form's schema so values can be labelled.
Reservations
/client-data/reservationsJWT/client-data/reservations/:reservationIdJWT/client-data/reservationsJWT/client-data/reservationsJWT/client-data/reservations/:reservationIdJWT/client-data/reservations/available-slotsJWTDelegates to the reservations service (CRM). Reading one by id needs a customer session.
Tickets, messages and notifications
/client-data/ticketsJWT/client-data/tickets/:ticketNumberJWT/client-data/ticketsJWT/client-data/tickets/with-attachmentsJWT/client-data/ticketsJWT/client-data/messagesJWT/client-data/messagesJWT/client-data/messages/:messageId/status/:statusJWT/client-data/conversationsJWT/client-data/notificationsJWT/client-data/notifications/push-tokenJWTTickets go through the CRM ticket desk: a customer sees only their own. with-attachments is multipart. Messages are the CRM inbox, newest first by createdate; ?conversationWith= narrows to one thread.
Addresses and wishlists
/client-data/addressesJWT/client-data/addressesJWT/client-data/addresses/:addressIdJWT/client-data/addresses/autocompleteJWT/client-data/addresses/place/:placeIdJWT/client-data/wishlistJWT/client-data/wishlistJWT/client-data/wishlist/:productIdJWT/client-data/wishlist/:productIdJWT/client-data/wishlist-list/:wishlistIdJWTSaving an address that matches an existing one updates it instead of duplicating. autocomplete ({ input, sessiontoken?, components? }) and place/:placeId are Google Places lookups.
A customer may own several named sf_wishlist records. POST wishlist/:productId adds a SKU (idempotent) to wishlistId from the body, or to their oldest wishlist, creating one if none exists. A wishlistId that belongs to someone else is 403.
Files
/client-data/filesJWT/client-data/files/uploadJWT/client-data/files/:pathJWTEach customer owns a private folder, client-account/<identity>/, chosen from the session and never from the request. Upload is multipart field file, with an optional location sub-folder. The list returns signed URLs valid for 15 minutes. Paths with .., encoded separators or another customer's prefix are 400.
Benefits
/client-data/benefitsJWT/client-data/benefits/enrollJWT/client-data/benefits/enrollmentsJWTbenefits returns the customer's benefits (from their groups and those attached directly), their enrollments split into pending, active and rejected, and availableBenefits they can still apply for. enroll takes { benefit, documents?, notes?, formData?, agreementAccepted? }; IP address and user agent are recorded.
Shared accounts
/client-data/shared-accounts/:accountId/membersJWT/client-data/shared-accounts/:accountId/ordersJWT/client-data/shared-accounts/:accountId/members/:associationIdJWT/client-data/shared-accounts/:accountId/invitesJWT/client-data/shared-accounts/invites/validate/:invitationTokenNo auth/client-data/shared-accounts/invites/accept/:invitationTokenJWTManaging needs an active manager association (customer_association) on that account — a buyer is refused 403, not shown a filtered list. PUT takes { role: manager | buyer, status: active | suspended }; the account must keep at least one manager. The invite link is validated publicly before sign-up; accepting happens once the invitee is signed in.
Payment links
/client-data/pay/:idJWT/client-data/pay/:id/intentJWT/client-data/pay/:idJWT:id is a payment token, an sf_transaction or an sf_invoice, looked up in that order. For an invoice, the paid amount and balance are worked out from the payments recorded against it, never its stored amountPaid. Gateways come back with public keys only; a half-configured gateway is left out.
The amount is always the server's balance: intent answers { alreadySettled: true } when nothing is owed, and POST pay/:id ({ gateway, ref, method?, email?, name? }) records the payment against the balance, so a settled link cannot collect twice. The payer is the signed-in customer, or the email given.
Also here
/client-data/payment-methodsJWT/client-data/payment-methodsJWT/client-data/payment-methods/:paymentMethodIdJWT/client-data/payment-methods/:paymentMethodId/defaultJWT/client-data/analyticsJWT/client-data/expert/messagesJWT/client-data/expert/messagesJWT/client-data/expert/hireJWTThe payment-method routes do nothing (GET returns []). analytics returns zeros. GET transactions and GET expert/messages return no data. The email verification routes and phone send return success without sending or checking a code. Do not build on these.
The expert/* routes write messages and tickets to the platform's shared org (SHARED_ORG), not the site's own org.