docs
/
Appmint Mobile

Endpoints

Every route Appmint Mobile calls, grouped by module, with the auth each one actually enforces.

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().

`/storefront` is public by default

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

POST/profile/user/directory/lookup
POST/profile/user/signin
POST/profile/user/signup
GET/profile/user/magic-link
POST/profile/user/magic-link/redirect
POST/profile/user/signin/passcode
POST/profile/user/refresh
GET/profileUSER
DELETE/profile/user/selfUSER

The 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

POST/profile/passcode/setUSER
POST/profile/passcode/removeUSER
POST/profile/passcode/card/registerUSER
POST/profile/passcode/card/statusUSER
GET/profile/passcode/cardsUSER
GET/profile/passcode/cards/:employeeIdUSER
GET/profile/passcode/card/:identifierUSER
GET/business-made/employeesUSER
GET/repository/find-by-attribute/setting/name/base-settingUSER

Card status is active, revoked or lost. The base-setting document carries data.passcodeLoginSettings {enable, mode}.

Storefront: POS tabs

GET/storefront/productsNo auth
GET/storefront/pos-categoriesNo auth
POST/storefront/pos/tabNo auth
GET/storefront/pos/tabsNo auth
GET/storefront/pos/tabs/closedNo auth
GET/storefront/order/:idNo auth
GET/storefront/order/:id/paymentsNo auth
POST/storefront/order/:id/itemsNo auth
POST/storefront/order/:id/fireNo auth
POST/storefront/pos/tab/:id/settleNo auth
POST/storefront/pos/tab/:id/refundNo auth
POST/storefront/pos/tab/:id/splitNo auth
POST/storefront/pos/tab/:id/assign-service-pointNo auth
POST/storefront/pos/tab/:id/release-service-pointNo auth
GET/storefront/pos/tab/:id/receipt-payloadNo auth
GET/storefront/pos/tab/:id/check-payloadNo auth
POST/storefront/pos/tab/:id/print-checkNo auth
POST/storefront/pos/tab/:id/print-receiptNo auth
POST/storefront/pos/tab/:id/send-receiptNo auth
POST/storefront/service-point/:id/statusNo auth

Open 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

GET/storefront/payment-gatewaysNo auth
POST/storefront/stripe/intentNo auth
POST/storefront/stripe/terminal/connection-tokenNo auth
POST/storefront/stripe/terminal/intentNo auth
POST/storefront/stripe/terminal/location/ensureNo auth

Gateways 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)

POST/repository/find/:datatypeUSER
GET/repository/get/:datatype/:idUSER
POST/repository/createUSER
POST/repository/update-partial/:datatype/:idUSER
DELETE/repository/delete/:datatype/:idUSER
GET/repository/find-by-attribute/:datatype/:attribute/:valueUSER

Used 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

GET/crm/reservations/definitionsNo auth
POST/crm/reservations/slotsNo auth
GET/crm/reservations/getCUSTOMER
GET/crm/reservations/get/:idCUSTOMER
POST/crm/reservations/createCUSTOMER
POST/crm/reservations/updateCUSTOMER
POST/crm/reservations/send-reminder/:reservationIdCUSTOMER
GET/crm/reservations/service-point/getCUSTOMER
GET/crm/reservations/service-point/get/:idCUSTOMER
POST/crm/reservations/service-point/createCUSTOMER
POST/crm/reservations/service-point/updateCUSTOMER
DELETE/crm/reservations/service-point/delete/:idCUSTOMER

The 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

POST/checkin/walk-inUSER
POST/checkin/from-reservation/:reservationIdUSER
POST/checkin/:taskId/assignUSER
POST/checkin/:taskId/leaveUSER
POST/checkin/:taskId/no-showUSER
POST/checkin/:taskId/notifyUSER
POST/checkin/service-point/:spId/clearUSER
GET/checkin/service-pointUSER
GET/checkin/service-point/availableUSER
GET/checkin/queueUSER
GET/checkin/queue/summaryUSER
GET/checkin/queue/historyUSER
GET/checkin/queue/position/:taskIdUSER
GET/checkin/upcomingUSER
GET/checkin/reservations/todayUSER

All 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

GET/workflow/definitionUSER
GET/workflow/taskUSER
POST/workflow/task/:id/advanceUSER
POST/workflow/task/:id/completeUSER
POST/workflow/task/:id/cancelUSER
POST/workflow/task/:id/move-to/:stageIdUSER
POST/workflow/task/:id/noteUSER
POST/workflow/task/:id/snoozeUSER
POST/workflow/task/:id/escalate-nowUSER
POST/workflow/task/:id/reassignUSER
GET/workflow/analytics/:workflowId/wait-timesUSER
GET/workflow/escalation/breachedUSER
GET/workflow/escalation/upcomingUSER

CRM

GET/crm/leads/detailUSER
POST/crm/leads/detailUSER
GET/crm/leads/detail/:idUSER
PUT/crm/leads/detail/:idUSER
DELETE/crm/leads/detail/:idUSER
GET/crm/tickets/messages/:idUSER
POST/crm/tickets/reply/:idUSER
GET/crm/tickets/comments/:idUSER
POST/crm/tickets/comments/:idUSER
GET/crm/customer-activity/:email/timelineUSER
GET/crm/customer-activity/:email/summaryUSER
POST/crm/customer-activity/eraseUSER
GET/crm/inbox/conversationsUSER
GET/crm/inbox/conversations/:partiesUSER
POST/crm/inbox/updateUSER
GET/crm/communications/smsUSER
GET/crm/communications/callsUSER
GET/crm/communications/allUSER
GET/crm/communications/recordingsUSER
GET/crm/communications/recordings/:sidUSER

POST /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

GET/chat/sessionsUSER
GET/chat/history/:chatIdUSER
GET/chat/messages/:emailUSER
GET/chat/config/:chatIdNo auth
GET/chat/agents/onlineUSER
GET/chat/agents/:email/presenceUSER
POST/chat/agents/:email/statusUSER
GET/chat/customers/onlineUSER
GET/chat/customers/:email/journeyUSER
GET/chat/presence/statsUSER
GET/chat/queueUSER
POST/chat/queueUSER
GET/chat/queue/statsUSER
GET/chat/queue/position/:chatIdUSER

Socket.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

POST/phone/voice/register-deviceUSER
POST/phone/voice/heartbeatUSER
POST/phone/voice/unregister-deviceUSER
GET/phone/voice/devicesUSER
POST/phone/tokenUSER
GET/phone/numbersUSER
GET/phone/user-phonesUSER
GET/phone/systemUSER

Register 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)

GET/eventsUSER
POST/eventsUSER
GET/events/:idUSER
PUT/events/:idUSER
DELETE/events/:idUSER
POST/events/:id/publishUSER
GET/events/:id/ticketsUSER
POST/events/:id/ticketsUSER
POST/events/tickets/validateUSER
GET/events/:id/ticket-typesUSER
GET/events/tickets/:id/qrUSER
POST/events/checkinUSER
POST/events/checkoutUSER
GET/events/:id/checkin-statsUSER
GET/events/:id/occupancyUSER
GET/events/:id/sessionsUSER
POST/events/:id/sessionsUSER
PUT/events/sessions/:idUSER
DELETE/events/sessions/:idUSER
GET/events/:id/scheduleUSER
GET/events/:id/participantsUSER
POST/events/:id/participantsUSER
PUT/events/participants/:idUSER
DELETE/events/participants/:idUSER
POST/events/participants/:id/confirmUSER
POST/events/tickets/purchaseUSER
POST/events/tickets/compUSER
POST/events/tickets/fulfillUSER
GET/events/tickets/lookup/:eventIdUSER
POST/events/tickets/:id/activateUSER
POST/events/tickets/:id/transferUSER
POST/events/tickets/:id/refundUSER
DELETE/events/tickets/:idUSER
GET/events/tickets/:id/badgeUSER
POST/events/tickets/:id/badge/printedUSER
POST/events/tickets/:id/perks/:perkId/claimUSER
GET/events/:id/mediaUSER
POST/events/:id/mediaUSER
POST/events/:id/media/shareUSER

The 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/checkin and POST /events/checkout take {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 is success:false with reason, not an HTTP error.
  • GET /events/:id/checkin-stats returns {total, successful, denied, uniqueAttendees, byZone, byHour}. The app's "Checked In" is uniqueAttendees (fallback successful); "Scans" is total.
  • GET /events/:id/occupancy returns an object keyed by zone id: {zoneId, zoneName, capacity, currentOccupancy, availableCapacity, percentFull, totalEntranceScans, totalEligibilityScans}. Scans sent without a zone are logged under zone unknown and are not in it.
  • GET /events/tickets/:id/qr returns the ticket with a freshly signed data.code; a ticket created without a codeSecret gets one on first call.
  • POST /events/:id/tickets needs ticketTypeId; POST /events/tickets/comp needs items:[{ticketTypeId, quantity, holderEmail, holderName?}]; POST /events/tickets/fulfill with walkIn needs walkIn.ticketTypeId. The app loads the choices from GET /events/:id/ticket-types.

Social sync

GET/sync/social-activities/commentsUSER
GET/sync/social-activities/messagesUSER
GET/sync/social-activities/engagementUSER