All requests go to EnvironmentConfig.appengineEndpoint with Authorization: Bearer <token> and an orgid header. Auth in the blocks below is what the server enforces: user means a staff JWT (@Roles(RoleType.User)), customer means any authenticated customer or staff token, public means @PublicRoute().
StorefrontController carries a class-level @PublicRoute(). Every POS tab, settle, refund, receipt, service-point status and /storefront/take-payment route is reachable without a token; identity is only attached when a token is present. Only POST /storefront/categories is role-gated. Put the API behind network controls in production and do not assume the token protects these calls.
Auth and profile
/profile/user/directory/lookup/profile/user/signin/profile/user/signup/profile/user/magic-link/profile/user/magic-link/redirect/profile/user/signin/passcode/profile/user/refresh/profileUSER/profile/user/selfUSERThe directory lookup is step one of sign-in. It is public — no org header, no token — takes {email}, and answers {orgs: [{orgId, displayName}]}, most recently used first. That is how the app knows which organization to sign into without anyone typing one: an empty list means no such account, one entry is used silently, several are offered as a picker. The orgid header on the sign-in call that follows carries the chosen org.
Sign-in sends {orgId, email, password}; the magic-link GET takes ?email=&type=code and the redirect POST takes {email, code}. Passcode sign-in takes {employeeId, pin?, cardUid?} and uses the org the device last signed into. Delete-self is refused for the org's primary contact.
Passcodes and access cards
/profile/passcode/setUSER/profile/passcode/removeUSER/profile/passcode/card/registerUSER/profile/passcode/card/statusUSER/profile/passcode/cardsUSER/profile/passcode/cards/:employeeIdUSER/profile/passcode/card/:identifierUSER/business-made/employeesUSER/repository/find-by-attribute/setting/name/base-settingUSERCard status is active, revoked or lost. The base-setting document carries data.passcodeLoginSettings {enable, mode}.
Storefront: POS tabs
/storefront/productsNo auth/storefront/pos-categoriesNo auth/storefront/pos/tabNo auth/storefront/pos/tabsNo auth/storefront/pos/tabs/closedNo auth/storefront/order/:idNo auth/storefront/order/:id/paymentsNo auth/storefront/order/:id/itemsNo auth/storefront/order/:id/fireNo auth/storefront/pos/tab/:id/settleNo auth/storefront/pos/tab/:id/refundNo auth/storefront/pos/tab/:id/splitNo auth/storefront/pos/tab/:id/assign-service-pointNo auth/storefront/pos/tab/:id/release-service-pointNo auth/storefront/pos/tab/:id/receipt-payloadNo auth/storefront/pos/tab/:id/check-payloadNo auth/storefront/pos/tab/:id/print-checkNo auth/storefront/pos/tab/:id/print-receiptNo auth/storefront/pos/tab/:id/send-receiptNo auth/storefront/service-point/:id/statusNo authOpen a tab with {businessLocationId, tabLabel?, servicePointId?, customer?}. Settle takes {amount, method?, gateway?, ref?} and returns {order, transaction} — the app unwraps order. Fire is idempotent: lines that already carry a taskId are dropped. Split takes itemIds or parts. Receipt payloads accept ?copy=customer|merchant.
Storefront: payments
/storefront/payment-gatewaysNo auth/storefront/stripe/intentNo auth/storefront/stripe/terminal/connection-tokenNo auth/storefront/stripe/terminal/intentNo auth/storefront/stripe/terminal/location/ensureNo authGateways return the provider name and publishable key (StripeProvider, PayPalProvider, HelcimProvider). Terminal intents are created with payment_method_types: ['card_present']; the resulting PaymentIntent id is passed to settle as ref.
Repository (generic records)
/repository/find/:datatypeUSER/repository/get/:datatype/:idUSER/repository/createUSER/repository/update-partial/:datatype/:idUSER/repository/delete/:datatype/:idUSER/repository/find-by-attribute/:datatype/:attribute/:valueUSERUsed for sf_product, bm_location_product, sf_order edits, reservation, service_point, location, customer, task, ticket, message. update-partial takes a flat key map that the server $sets verbatim — write data.productItems, not productItems, or the value lands at the record root. It returns a boolean, not the record.
Reservations and service points
/crm/reservations/definitionsNo auth/crm/reservations/slotsNo auth/crm/reservations/getCUSTOMER/crm/reservations/get/:idCUSTOMER/crm/reservations/createCUSTOMER/crm/reservations/updateCUSTOMER/crm/reservations/send-reminder/:reservationIdCUSTOMER/crm/reservations/service-point/getCUSTOMER/crm/reservations/service-point/get/:idCUSTOMER/crm/reservations/service-point/createCUSTOMER/crm/reservations/service-point/updateCUSTOMER/crm/reservations/service-point/delete/:idCUSTOMERThe app lists reservations through POST /repository/find/reservation rather than /crm/reservations/get, because the latter returns only the caller's own bookings. create expects flat fields; a {datatype, data} body is rejected as "not new". Slots take {reservationDefinitionId, serviceName?, serviceDate, partySize?}.
Check-in queue
/checkin/walk-inUSER/checkin/from-reservation/:reservationIdUSER/checkin/:taskId/assignUSER/checkin/:taskId/leaveUSER/checkin/:taskId/no-showUSER/checkin/:taskId/notifyUSER/checkin/service-point/:spId/clearUSER/checkin/service-pointUSER/checkin/service-point/availableUSER/checkin/queueUSER/checkin/queue/summaryUSER/checkin/queue/historyUSER/checkin/queue/position/:taskIdUSER/checkin/upcomingUSER/checkin/reservations/todayUSERAll routes need a JWT but no role. Walk-in requires at least one of name, email or phone. From-reservation accepts {partySize?, businessLocationId?, notes?} and answers 409 while a live task already exists. Assign takes {servicePointId} (name or id; the server writes back by id) and rejects points that are occupied or reserved. Clear takes {finalStatus: 'dirty' | 'available'}.
Workflow
/workflow/definitionUSER/workflow/taskUSER/workflow/task/:id/advanceUSER/workflow/task/:id/completeUSER/workflow/task/:id/cancelUSER/workflow/task/:id/move-to/:stageIdUSER/workflow/task/:id/noteUSER/workflow/task/:id/snoozeUSER/workflow/task/:id/escalate-nowUSER/workflow/task/:id/reassignUSER/workflow/analytics/:workflowId/wait-timesUSER/workflow/escalation/breachedUSER/workflow/escalation/upcomingUSERCRM
/crm/leads/detailUSER/crm/leads/detailUSER/crm/leads/detail/:idUSER/crm/leads/detail/:idUSER/crm/leads/detail/:idUSER/crm/tickets/messages/:idUSER/crm/tickets/reply/:idUSER/crm/tickets/comments/:idUSER/crm/tickets/comments/:idUSER/crm/customer-activity/:email/timelineUSER/crm/customer-activity/:email/summaryUSER/crm/customer-activity/eraseUSER/crm/inbox/conversationsUSER/crm/inbox/conversations/:partiesUSER/crm/inbox/updateUSER/crm/communications/smsUSER/crm/communications/callsUSER/crm/communications/allUSER/crm/communications/recordingsUSER/crm/communications/recordings/:sidUSERPOST /crm/inbox/update?send=true wraps the message in a record and triggers the send; it is also how the SMS composer sends. Inbox conversations are threaded server-side as contact plus channel.
Chat and presence
/chat/sessionsUSER/chat/history/:chatIdUSER/chat/messages/:emailUSER/chat/config/:chatIdNo auth/chat/agents/onlineUSER/chat/agents/:email/presenceUSER/chat/agents/:email/statusUSER/chat/customers/onlineUSER/chat/customers/:email/journeyUSER/chat/presence/statsUSER/chat/queueUSER/chat/queueUSER/chat/queue/statsUSER/chat/queue/position/:chatIdUSERSocket.IO namespace /chat, websocket transport, auth {token, orgId, chatId?}. The app emits sendMessage, chat-message, chatRequest, updateMessageStatus, shareStatus, getMessages, set-status, pick-next, transfer-chat, close-chat, takeover-chat, resume-ai, assist-ai, join-chat, leave-chat, and listens for authenticate, message, update, status, messages, ai-stream, chat-stream, presence-change, chat-assigned, chat-transferred, chat-ended, agent-assigned, agent-changed, queue-notification, queue-updated, session-expiring, session-expired, token_expired, chat-inactivity, sms.received.
Phone and softphone
/phone/voice/register-deviceUSER/phone/voice/heartbeatUSER/phone/voice/unregister-deviceUSER/phone/voice/devicesUSER/phone/tokenUSER/phone/numbersUSER/phone/user-phonesUSER/phone/systemUSERRegister with {deviceId, platform, label, capabilities: ['voice','sms'], clientInfo}; the response carries the Twilio access token and assigned numbers. Heartbeat every 60 s against a 90 s server TTL.
Events (staff side)
/eventsUSER/eventsUSER/events/:idUSER/events/:idUSER/events/:idUSER/events/:id/publishUSER/events/:id/ticketsUSER/events/:id/ticketsUSER/events/tickets/validateUSER/events/:id/ticket-typesUSER/events/tickets/:id/qrUSER/events/checkinUSER/events/checkoutUSER/events/:id/checkin-statsUSER/events/:id/occupancyUSER/events/:id/sessionsUSER/events/:id/sessionsUSER/events/sessions/:idUSER/events/sessions/:idUSER/events/:id/scheduleUSER/events/:id/participantsUSER/events/:id/participantsUSER/events/participants/:idUSER/events/participants/:idUSER/events/participants/:id/confirmUSER/events/tickets/purchaseUSER/events/tickets/compUSER/events/tickets/fulfillUSER/events/tickets/lookup/:eventIdUSER/events/tickets/:id/activateUSER/events/tickets/:id/transferUSER/events/tickets/:id/refundUSER/events/tickets/:idUSER/events/tickets/:id/badgeUSER/events/tickets/:id/badge/printedUSER/events/tickets/:id/perks/:perkId/claimUSER/events/:id/mediaUSER/events/:id/mediaUSER/events/:id/media/shareUSERThe whole /events controller is staff-only. The attendee app uses the separate /client/events surface documented under EventOxygen.
Shapes the app depends on:
POST /events/checkinandPOST /events/checkouttake{code, zone?, checkpoint?}; checkout also accepts{ticketId}. The code is resolved as a dynamic QR payload (ticketId:timestamp:signature), then as a static ticket code (case-insensitive), then as a credential code. Both answer{success, ticket?, reason?}; a denial issuccess:falsewithreason, not an HTTP error.GET /events/:id/checkin-statsreturns{total, successful, denied, uniqueAttendees, byZone, byHour}. The app's "Checked In" isuniqueAttendees(fallbacksuccessful); "Scans" istotal.GET /events/:id/occupancyreturns an object keyed by zone id:{zoneId, zoneName, capacity, currentOccupancy, availableCapacity, percentFull, totalEntranceScans, totalEligibilityScans}. Scans sent without a zone are logged under zoneunknownand are not in it.GET /events/tickets/:id/qrreturns the ticket with a freshly signeddata.code; a ticket created without acodeSecretgets one on first call.POST /events/:id/ticketsneedsticketTypeId;POST /events/tickets/compneedsitems:[{ticketTypeId, quantity, holderEmail, holderName?}];POST /events/tickets/fulfillwithwalkInneedswalkIn.ticketTypeId. The app loads the choices fromGET /events/:id/ticket-types.
Social sync
/sync/social-activities/commentsUSER/sync/social-activities/messagesUSER/sync/social-activities/engagementUSER