Skip to content

API Endpoints Reference

All API endpoints are served from the /api prefix. The base URL depends on the environment:

  • Local: http://localhost:8000/api
  • Staging: https://api.staging.upvendo.com/api
  • Production: https://api.upvendo.com/api

The one documented exception is GET /version, registered in routes/web.php and therefore served without the /api segment -- see Service Version.

Route Architecture

Routes are organized in a modular file structure under routes/:

  • routes/api.php - Main API route file, includes all sub-route files
  • routes/web.php - Non-/api web routes: /health, the public /version commit-hash endpoint, the receipt view, and (test envs only) the monitoring dashboard
  • routes/api/guest.php - Unauthenticated routes (login, webhooks, OAuth callbacks, device activation)
  • routes/api/field.php - Field Ops app routes (FieldSession audience, /field prefix)
  • routes/api/backoffice/*.php - BackOffice CRUD routes (auto-loaded via glob)
  • routes/api/backoffice/settings/*.php - Settings sub-routes (auto-loaded via glob from settings.php)
  • routes/api/reseller/commissions.php - Reseller commission routes (auth:sanctum)
  • routes/api/pos.php - First-party POS device routes (/pos prefix, POS device JWT)
  • routes/api/print.php - Thermal-printer poll route (/print prefix), mounted outside every auth group
  • routes/api/dev/*.php - Dev/diagnostic routes for Lightspeed, MplusKassa, ShopCaisse and print. Test environments only, and additionally gated by e2e.auth

All backoffice routes live under the /back-office prefix and require the middleware stack: auth, type:backoffice, tenant:backoffice, check-user-activity

(Verified: routes/api.php -- the /back-office group and its glob(base_path('routes/api/backoffice/*.php')) loop; routes/api/backoffice/settings.php for the nested glob; routes/api/guest.php for the GeneralHelper::isTestEnv() + e2e.auth gate around the dev routes.)

The POS API is on production. routes/api/pos.php is required at routes/api.php:239 and app/Http/Controllers/Api/Pos/ holds eight controllers. See POS Endpoints below, and the back-office POS/KDS prefixes under Printer, KDS & POS Admin. Note that every back-office POS/KDS prefix except POST /pos/select-provider additionally carries first-party-pos (EnsureFirstPartyPos), which 403s any merchant whose pos_provider is not upvendo — and in production only test-flagged merchants can currently become one (upvendo is staged 'test_only' => true, config/pos-providers.php:52). These endpoints are staged, not generally available.


Public Endpoints (No Auth Required)

Constants

MethodPathDescription
GET/country-options/{forPaymentProfile?}List available countries (parameter is optional)
GET/lang-optionsList available languages
GET/calling-code-optionsList phone calling codes
GET/branding-font-optionsList branding font choices
GET/allergens-optionsList allergen options
GET/dietary-preferences-optionsList dietary preference options
GET/dietary-supplements-optionsList dietary supplement options
GET/tags-optionsList tag options

Health Check

MethodPathDescription
GET/healthSimple health check
GET/health/readinessReadiness probe
GET/health/detailedDetailed health check with component status
GET/health/freshFresh health check (no cache)
GET/health/statusHealth status summary

(Verified: routes/api/guest.php -- the HealthController block inside the guest group.)

Service Version

MethodPathDescription
GET/versionCommit currently serving. Public, unauthenticated, always 200

Not under /api. This route is registered in routes/web.php, not routes/api.php, so the path is https://api.upvendo.com/version -- no /api segment, no middleware, no auth. It is also deliberately not /version.json: that name belongs to the frontends' separate plain-text version-check contract.

Response body (VersionController::__invoke):

json
{
  "app": "upvendo-backend",
  "commit": "e9967d3f6a1b2c3d4e5f60718293a4b5c6d7e8f9",
  "short": "e9967d3",
  "branch": "production",
  "environment": "production",
  "deployed_at": "2026-08-09T06:31:04+00:00"
}
  • commit / short / branch fall back to the literal string "unknown" when the commit cannot be resolved; deployed_at is null in that case. The endpoint never 500s and never fails a deploy over a version string -- it reports what is running, it is not a health check.
  • Resolution order (first non-empty wins): env('APP_VERSION') (verbatim, branch = "unknown"), then <base>/.commit (line 1 = sha, line 2 = branch; deployed_at = the file's mtime), then a direct read of <base>/.git (HEAD, loose ref, then packed-refs), then "unknown". git is never shelled out to, and nothing touches the database.
  • A detached HEAD yields the sha with branch = "unknown".
  • environment is app()->environment().

(Verified: routes/web.php:19; app/Http/Controllers/VersionController.php:16-28; app/Support/AppVersion.php:18-247. Upstream: upvendo-backend PR #1637.)


Authentication Endpoints

BackOffice Authentication (Guest Routes)

MethodPathDescription
POST/loginLogin with email/password, returns JWT token (or verification_required if the email is unverified)
POST/login/googleSign in with a Google Identity Services credential (throttle 5/min)
POST/back-office/registerRegister new merchant account — returns verification_required, no token
POST/back-office/verify-emailConsume the emailed verification token ({ token })
POST/back-office/resend-verificationRe-send the verification email ({ email })
POST/back-office/forget-passwordRequest password reset email
PUT/back-office/update-passwordUpdate password using reset token
POST/back-office/request-otpRequest OTP code for two-factor auth
POST/back-office/verify-otpVerify OTP code and complete login
POST/back-office/passkeysGet WebAuthn challenge for passkey login
POST/back-office/authenticate-passkeyAuthenticate using WebAuthn passkey
GET/back-office/join-merchantGet details for merchant invitation
POST/back-office/join-merchantAccept merchant invitation

BackOffice Authenticated User

MethodPathMiddlewareDescription
POST/logoutauth, type:backofficeLogout (client discards JWT)
POST/back-office/start-usingauth, type:backofficeMark merchant as started
GET/back-office/userauth, type:backofficeGet current user profile
PUT/back-office/user-personal-infoauth, type:backofficeUpdate personal info
PUT/back-office/user-business-infoauth, type:backofficeUpdate business info
GET/back-office/merchant-optionsauth, type:backofficeList user's merchants
POST/back-office/set-merchantauth, type:backofficeSwitch active merchant (returns new JWT)
GET/back-office/bootstrapauth, type:backofficeSingle-request boot payload for the back-office SPA
GET/validate-tokenauthValidate JWT token (used by Cloudflare Worker)

GET /back-office/bootstrap

A fan-in boot endpoint: it aggregates what the SPA otherwise fetches in roughly nine separate requests (user, merchant options, location options, unread notifications, feature flags, and the location-scoped channel / pricing / ordering options) so that the auth + tenant + framework overhead is paid once. Each slice reuses the same service the individual endpoint uses.

It carries no permission gate (none of its sources has one -- they self-filter internally), and it is deliberately not under admin-vendor-override. That is intentional: the boot payload must only ever reflect the caller's own JWT-bound merchant. An admin or reseller switches merchant via POST /back-office/set-merchant, which mints a new JWT, rather than by passing ?vendor_id here.

(Verified: routes/api/backoffice/bootstrap.php -- the route and its explanatory comment; app/Http/Controllers/Api/BackOffice/BootstrapController.php; assembly in app/Services/Orchestrators/BackOffice/BootstrapOrchestrator.php.)

Passkey Management (Authenticated)

MethodPathDescription
GET/back-office/passkeysGet user's registered passkeys
GET/back-office/passkeys/setupGet passkey registration options
POST/back-office/passkeys/setupRegister a new passkey
DELETE/back-office/passkeysDelete a passkey

Customer Authentication (Guest Routes)

MethodPathDescription
POST/customer/send-otpSend OTP to customer phone/email. Additionally carries verify.turnstile:storefront_secret (routes/api/guest.php:110-111); accepts an optional body field cf-turnstile-response and answers 403 CAPTCHA_REQUIRED (no token) or CAPTCHA_INVALID (bad token / Cloudflare unreachable) — but only while TURNSTILE_STOREFRONT_SECRET_KEY is set. Empty secret ⇒ the middleware passes through untouched.
POST/customer/loginVerify OTP and login customer
POST/customer/login/googleSign in with a Google ID token (credential, optional location_id)

The whole /customer guest prefix carries throttle:20,1 (20 req/min per IP). Both OTP routes are unauthenticated and abuse-prone — send-otp creates a Customer and sends an SMS/email per call, and login has no per-attempt counter, so a 6-digit OTP would be grindable for its full 15-minute validity.

Customer Authenticated

MethodPathMiddlewareDescription
GET/customer/userauth, type:customerGet customer profile
POST/customer/logoutauth, type:customerLogout customer
GET/customer/personal-infoauth, type:customerGet personal info
POST/customer/personal-infoauth, type:customerUpdate personal info
GET/customer/addressesauth, type:customerList addresses
GET/customer/addresses/optionsauth, type:customerAddress form options
POST/customer/addressesauth, type:customerAdd new address
DELETE/customer/addresses/{addressId}auth, type:customerDelete address
GET/customer/loyalties/{slug}auth, type:customerList loyalty programs
GET/customer/loyalties/{slug}/{locationId}auth, type:customerLoyalty program detail
GET/customer/{slug}/locationsauth, type:customerList user's locations

Device Authentication (Guest)

MethodPathMiddlewareDescription
POST/device-auth/activate--Activate device with activation code
GET/device-auth/admin-optionsverify.admin-switch-ipList switch targets for an on-site admin
POST/device-auth/admin-switchverify.admin-switch-ipSwitch the device to another target

Device Authenticated

MethodPathMiddlewareDescription
GET/device-auth/userauth, tenant:kiosk,kdsGet device info
POST/device-auth/network-infoauth, tenant:kiosk,kdsUpdate device network info
POST/device-auth/heartbeatauth, tenant:kiosk,kdsDevice heartbeat / liveness ping
POST/device-auth/logoutauthLogout device

(Verified: guest routes in routes/api/guest.php -- the /device-auth prefix group; authenticated routes in routes/api.php -- the /device-auth group under auth.)


BackOffice CRUD Endpoints

All endpoints below are prefixed with /back-office and require auth + type:backoffice + tenant:backoffice middleware.

Items

MethodPathPermissionDescription
GET/items/datatableVIEW_ITEMSList items (DataTable format)
GET/items/exportEXPORT_ITEMSExport items to spreadsheet
GET/items/options--Item select options
POST/itemsCREATE_ITEMSCreate item
GET/items/{id}VIEW_ITEMSShow item detail
PUT/items/{id}EDIT_ITEMSUpdate item
DELETE/items/{id}DELETE_ITEMSDelete item

Categories

MethodPathPermissionDescription
GET/categories/datatableVIEW_CATEGORIESList categories
GET/categories/options--Category select options
POST/categoriesCREATE_CATEGORIESCreate category
GET/categories/{id}VIEW_CATEGORIESShow category
PUT/categories/{id}EDIT_CATEGORIESUpdate category
DELETE/categories/{id}DELETE_CATEGORIESDelete category
MethodPathPermissionDescription
GET/menu-options--Menu select options
GET/menusVIEW_MENUSList all menus
POST/menusCREATE_MENUSCreate menu
GET/menus/{id}VIEW_MENUSShow menu detail
PUT/menus/{id}EDIT_MENUSUpdate menu
DELETE/menus/{id}DELETE_MENUSDelete menu
POST/menus/duplicate/{id}CREATE_MENUSDuplicate menu
POST/menus/draft/{id}EDIT_MENUSSet menu to draft
POST/menus/archive/{id}EDIT_MENUSArchive menu
POST/menus/publish/{id}EDIT_MENUSPublish menu
GET/menus/display-group-items/{id}VIEW_MENUSList display group items
POST/menus/display-group-items/{id}EDIT_MENUSSave display group items
DELETE/menus/display-groups/{id}EDIT_MENUSDelete display group
POST/menus/move-item-to-other-group/{id}EDIT_MENUSMove item between groups
POST/menus/add-item-to-groups/{itemId}EDIT_MENUSAppend an item to one or more display groups (does not remove it from its current group)
POST/menus/display-group-items/{id}/draftEDIT_MENUSSave display-group items as a draft
POST/menus/display-group-items/{id}/discardEDIT_MENUSDiscard the display-group-items draft

Modifier Groups

MethodPathPermissionDescription
GET/modifiers--List all modifiers
GET/modifier-groupsVIEW_MODIFIER_GROUPSList modifier groups
POST/modifier-groupsCREATE_MODIFIER_GROUPSCreate modifier group
GET/modifier-groups/options--Modifier group options
PUT/modifier-groups/reorderEDIT_MODIFIER_GROUPSReorder modifier groups
GET/modifier-groups/{id}VIEW_MODIFIER_GROUPSShow modifier group
PUT/modifier-groups/{id}EDIT_MODIFIER_GROUPSUpdate modifier group
DELETE/modifier-groups/{id}DELETE_MODIFIER_GROUPSDelete modifier group

Variant Groups

MethodPathPermissionDescription
POST/variant-groupsCREATE_VARIANT_GROUPSCreate variant group
GET/variant-groups/{id}VIEW_ITEMSShow variant group
PUT/variant-groups/{id}EDIT_VARIANT_GROUPSUpdate variant group
DELETE/variant-groups/{id}DELETE_VARIANT_GROUPSDelete variant group

Display Groups

MethodPathDescription
GET/display-groupsList all display groups
POST/display-groupsCreate display group
GET/display-groups/{id}Show display group
PUT/display-groups/{id}Update display group
DELETE/display-groups/{id}Delete display group

Offers

MethodPathPermissionDescription
GET/offersVIEW_OFFERSList offers
POST/offersCREATE_OFFERSCreate offer
GET/offers/{id}VIEW_OFFERSShow offer
PUT/offers/{id}EDIT_OFFERSUpdate offer
DELETE/offers/{id}DELETE_OFFERSDelete offer
POST/offers/activate/{id}EDIT_OFFERSActivate offer
POST/offers/deactivate/{id}EDIT_OFFERSDeactivate offer
POST/offers/archive/{id}EDIT_OFFERSArchive offer
POST/offers/unarchive/{id}EDIT_OFFERSUnarchive offer

Transactions

MethodPathPermissionDescription
GET/transactions/exportEXPORT_TRANSACTION_REPORTSExport transactions
GET/transactions/statsVIEW_TRANSACTIONSTransaction statistics
GET/transactions/datatableVIEW_TRANSACTIONSList transactions
GET/transactions/{id}VIEW_TRANSACTIONSShow transaction
POST/transactions/{id}/resend-receiptVIEW_TRANSACTIONSResend receipt
POST/transactions/{id}/retry-syncVIEW_TRANSACTIONSRetry a failed POS sync
POST/transactions/{id}/verify-payment-statusVIEW_TRANSACTIONSRecheck the payment at Stripe and settle if paid
DELETE/transactions/{id}VIEW_TRANSACTIONSDelete a test-mode transaction

Async Monitoring

The whole prefix sits behind the global-admin middleware — it exposes cross-tenant job internals.

MethodPathPermissionDescription
GET/async-monitoringglobal-admin (role)Monitor index
GET/async-monitoring/summaryglobal-admin (role)Summary + recent runs
GET/async-monitoring/queue?limit={n}global-admin (role)Live queue and failed-job tables

VAT Verification

MethodPathPermissionDescription
POST/settings/vat/verifyBack-office auth only (throttle 30/min)VIES VAT live check; fails open — always 200 with valid / invalid / unavailable / skipped

Customers (BackOffice)

MethodPathPermissionDescription
GET/customers/datatableVIEW_CUSTOMERSList customers
GET/customers/exportEXPORT_CUSTOMERSExport customers
POST/customersCREATE_CUSTOMERSCreate customer
GET/customers/order-detail/{id}VIEW_TRANSACTIONSShow order detail
GET/customers/{id}VIEW_CUSTOMERSShow customer
PUT/customers/{id}EDIT_CUSTOMERSUpdate customer
DELETE/customers/{id}DELETE_CUSTOMERSDelete customer
GET/customers/{id}/ordersVIEW_CUSTOMERSCustomer orders
GET/customers/{id}/gift-cardsVIEW_CUSTOMERSCustomer gift cards
GET/customers/{id}/reward-redemptionsVIEW_CUSTOMERSCustomer rewards
GET/customers/{id}/timelinesVIEW_CUSTOMERSCustomer timeline
POST/customers/{id}/notesEDIT_CUSTOMERSAdd customer notes
POST/customers/{id}/marketingEDIT_CUSTOMERSUpdate marketing preferences
POST/customers/{id}/addressesEDIT_CUSTOMERSAdd address
POST/customers/{id}/addresses/{addressId}EDIT_CUSTOMERSSet default address

Devices

MethodPathPermissionDescription
GET/devicesVIEW_DEVICESList devices
POST/devicesCREATE_DEVICESCreate device
POST/devices/subscribe/{locationId}CREATE_DEVICESSubscribe device
GET/devices/newCREATE_DEVICESNew device form data
GET/devices/options--Device select options
GET/devices/{id}VIEW_DEVICESShow device
PUT/devices/{id}EDIT_DEVICESUpdate device
DELETE/devices/{id}DELETE_DEVICESDelete device
POST/devices/{id}/remote-control--Send remote control command
POST/devices/{id}/square-device-code--Create Square device code
POST/devices/{id}/new-activation-code--Generate new activation code
POST/devices/{id}/send-activation-code--Send activation code via email
POST/devices/{id}/activate-payment--Activate payment reader
POST/devices/{id}/deactivate-payment--Deactivate payment reader
POST/devices/{id}/profileEDIT_DEVICESAssign device profile
PATCH/devices/{id}/reader/labelEDIT_DEVICESRename the paired card reader

The following 14 /devices routes additionally carry first-party-pos (EnsureFirstPartyPos); the pre-existing kiosk/terminal routes above do not.

MethodPathPermissionDescription
GET/devices/unconfigured-printersVIEW_DEVICESPrinters discovered but not yet configured
POST/devices/stationsEDIT_DEVICESCreate a KDS station
GET/devices/stations/location/{locationId}VIEW_DEVICESList a location's stations
GET/devices/stations/{id}VIEW_DEVICESShow station
PUT/devices/stations/{id}EDIT_DEVICESUpdate station
DELETE/devices/stations/{id}EDIT_DEVICESDelete station
POST/devices/{id}/stationEDIT_DEVICESBind a KDS device to a station (null unbinds)
POST/devices/{id}/printer/configureEDIT_DEVICESWrite a printer's configuration
POST/devices/{id}/printer/pairing-sessionEDIT_DEVICESStart pairing (mints/discloses the poll credential)
GET/devices/{id}/printer/pairing-sessionVIEW_DEVICESPairing status (read-only)
POST/devices/{id}/printer/pairing-session/confirm-writeEDIT_DEVICESConfirm the operator wrote the config
POST/devices/{id}/printer/test-printEDIT_DEVICESMerchant-facing test print
POST/devices/{id}/printer/rotate-tokenEDIT_DEVICESRotate the printer's poll token
POST/devices/{id}/printer/retireDELETE_DEVICESRetire the printer

(Verified: routes/api/backoffice/devices.php:40-41, :65-76, :94-95, :104-105, :135-153.)

Device Profiles

MethodPathPermissionDescription
GET/device-profilesVIEW_DEVICE_PROFILEList profiles
POST/device-profilesCREATE_DEVICE_PROFILECreate profile
GET/device-profiles/options--Profile options
GET/device-profiles/{id}VIEW_DEVICE_PROFILEShow profile
PUT/device-profiles/{id}EDIT_DEVICE_PROFILEUpdate profile
DELETE/device-profiles/{id}DELETE_DEVICE_PROFILEDelete profile

Printer Profiles, KDS Profiles, Kitchen Reports

All three prefixes carry first-party-pos (App\Http\Middleware\EnsureFirstPartyPos) on top of the per-route permission: a 403 unless the tenant merchant has pos_provider === 'upvendo', which only POST /pos/select-provider sets. They are staged Kitchen-Routing-v2 / KDS surface, not generally reachable by merchants who have not run first-party onboarding.

MethodPathPermissionDescription
GET/printer-profilesVIEW_DEVICE_PROFILEList printer profiles
POST/printer-profilesCREATE_DEVICE_PROFILECreate printer profile
GET/printer-profiles/optionsVIEW_DEVICE_PROFILEPrinter-profile options
GET/printer-profiles/{id}VIEW_DEVICE_PROFILEShow printer profile
PUT/printer-profiles/{id}EDIT_DEVICE_PROFILEUpdate printer profile
DELETE/printer-profiles/{id}DELETE_DEVICE_PROFILEDelete printer profile
GET/kds-profilesVIEW_DEVICE_PROFILEList KDS profiles
POST/kds-profilesCREATE_DEVICE_PROFILECreate KDS profile
GET/kds-profiles/optionsVIEW_DEVICE_PROFILEKDS-profile options
GET/kds-profiles/{id}VIEW_DEVICE_PROFILEShow KDS profile
PUT/kds-profiles/{id}EDIT_DEVICE_PROFILEUpdate KDS profile
DELETE/kds-profiles/{id}DELETE_DEVICE_PROFILEDelete KDS profile
GET/kds-reports/summaryVIEW_KITCHEN_REPORTSKitchen report summary

Deliberate divergence: the legacy kiosk /device-profiles/options above is ungated, while /printer-profiles/options and /kds-profiles/options both require VIEW_DEVICE_PROFILE.

(Verified: routes/api/backoffice/printer-profiles.php:37-53, routes/api/backoffice/kds-profiles.php:31-49, routes/api/backoffice/kds-reports.php:22-25.)

Back-Office POS Admin

POST /pos/select-provider is the one route in this family without first-party-pos — gating it would make it impossible to ever become first-party. Everything else carries it.

MethodPathPermissionDescription
POST/pos/select-providerEDIT_IN_HOUSE_CHANNELSelect the first-party Upvendo POS (sets the pos_provider marker)
GET/pos/staff-credentialsMANAGE_POS_STAFFList staff PIN credentials
GET/pos/staff-credentials/assignable-staffMANAGE_POS_STAFFUsers available for a credential
POST/pos/staff-credentialsMANAGE_POS_STAFFCreate a staff credential
PUT/pos/staff-credentials/{credentialId}MANAGE_POS_STAFFUpdate a staff credential
POST/pos/staff-credentials/{credentialId}/reset-pinMANAGE_POS_STAFFReset a staff PIN
DELETE/pos/staff-credentials/{credentialId}MANAGE_POS_STAFFDelete a staff credential
GET/pos/{locationId}/registersVIEW_POS_CONFIGURATIONList registers
POST/pos/{locationId}/registersMANAGE_POS_CONFIGURATIONCreate register
PUT/pos/{locationId}/registers/{registerId}MANAGE_POS_CONFIGURATIONUpdate register
POST/pos/{locationId}/registers/{registerId}/retireMANAGE_POS_CONFIGURATIONRetire register
POST/pos/{locationId}/registers/{registerId}/pair-terminalMANAGE_POS_CONFIGURATIONPair a Stripe Terminal reader
DELETE/pos/{locationId}/registers/{registerId}/terminalMANAGE_POS_CONFIGURATIONUnpair the reader
PATCH/pos/{locationId}/registers/{registerId}/terminal/labelMANAGE_POS_CONFIGURATIONRename the reader
GET/pos/{locationId}/drawersVIEW_POS_CONFIGURATIONList cash drawers
POST/pos/{locationId}/drawersMANAGE_POS_CONFIGURATIONCreate cash drawer
PUT/pos/{locationId}/drawers/{drawerId}MANAGE_POS_CONFIGURATIONUpdate cash drawer
POST/pos/{locationId}/drawers/{drawerId}/retireMANAGE_POS_CONFIGURATIONRetire cash drawer
GET/pos/{locationId}/fdm-devicesVIEW_POS_CONFIGURATIONList FDM devices
POST/pos/{locationId}/fdm-devicesMANAGE_POS_CONFIGURATIONCreate FDM device
PUT/pos/{locationId}/fdm-devices/{fdmDeviceId}MANAGE_POS_CONFIGURATIONUpdate FDM device
GET/pos/{locationId}/business-daysVIEW_POS_REPORTSList business days
GET/pos/{locationId}/business-days/{businessDayId}VIEW_POS_REPORTSShow a business day / Z-report
GET/pos/{locationId}/fiscal-eventsVIEW_POS_REPORTSFiscal journal export

The /pos/{locationId} group also carries location-owner; the whole file carries admin-vendor-override. MANAGE_POS_LAYOUTS gates the separate /pos/{locationId}/layouts prefix (routes/api/backoffice/pos-layouts.php).

(Verified: routes/api/backoffice/pos.php:39-112.)

Content

MethodPathPermissionDescription
GET/contents/datatable--Content datatable
GET/contents--List all content
POST/contentsCREATE_CONTENTCreate content
GET/contents/{id}VIEW_CONTENTShow content
POST/contents/{id}VIEW_CONTENTUpdate content
DELETE/contentsDELETE_CONTENTDelete multiple content items

Inventories

MethodPathDescription
GET/inventoriesInventory overview
POST/inventoriesCreate inventory history
DELETE/inventoriesDelete inventory entries

Tax Rates

MethodPathPermissionDescription
GET/tax-rate-options--Tax rate select options
GET/tax-rates/datatableVIEW_TAX_RATESList tax rates
POST/tax-ratesCREATE_CUSTOM_TAX_RATESCreate tax rate
PUT/tax-rates/{id}EDIT_CUSTOM_TAX_RATESUpdate tax rate
DELETE/tax-rates/{id}DELETE_CUSTOM_TAX_RATESDelete tax rate

Settings Endpoints

All prefixed with /back-office/settings/ and require backoffice auth.

Locations

MethodPathDescription
GET/location-optionsLocation select options
GET/locations/address-suggestionsAutocomplete address
GET/locations/datatableList locations
POST/locationsCreate location
GET/locations/{id}Show location
PUT/locations/{id}Update location
DELETE/locations/{id}Delete location
PUT/locations/{id}/statusUpdate location status
POST/locations/exportExport locations
GET/locations/{id}/terminal-optionsGet terminal options for location

Team (Users & Roles)

MethodPathPermissionDescription
GET/team/permission-list--List all permissions
GET/team/roles/options--Role select options
GET/team/rolesVIEW_ROLESList roles
GET/team/roles/{roleId}VIEW_ROLESShow role
POST/team/rolesCREATE_ROLESCreate role
PUT/team/roles/{roleId}EDIT_ROLESUpdate role
DELETE/team/roles/{roleId}DELETE_ROLESDelete role
GET/team/usersVIEW_USERSList users
POST/team/usersCREATE_USERSInvite user
POST/team/users/{userId}/resend-invitationCREATE_USERSResend invitation
PUT/team/users/{userId}/rolesASSIGN_ROLESAssign roles
PUT/team/users/{userId}/locationsEDIT_USER_LOCATION_ACCESSAssign locations
PUT/team/users/{userId}EDIT_USERSUpdate user
DELETE/team/users/{userId}DELETE_USERSDelete user
GET/team/users/globalVIEW_USERSList global users
POST/team/users/globalCREATE_USERSCreate global user
PUT/team/users/global/{userId}EDIT_USERSUpdate global user
DELETE/team/users/global/{userId}DELETE_USERSDelete global user

Billing Profiles

MethodPathPermissionDescription
GET/billing-profilesVIEW_BILLING_PROFILESList profiles
GET/billing-profiles/options--Profile options
POST/billing-profilesCREATE_BILLING_PROFILESCreate profile
GET/billing-profiles/{id}VIEW_BILLING_PROFILESShow profile
PUT/billing-profiles/{id}EDIT_BILLING_PROFILESUpdate profile name
POST/billing-profiles/{id}EDIT_BILLING_PROFILESAttach payment method
DELETE/billing-profiles/{id}DELETE_BILLING_PROFILESDelete profile
GET/billing-profiles/{id}/past-billsVIEW_BILLING_PROFILESPast bills datatable
GET/billing-profiles/{id}/exportVIEW_BILLING_PROFILESExport past bills
GET/billing-profiles/{id}/subscriptionsVIEW_BILLING_PROFILESList subscriptions
POST/billing-profiles/{id}/subscriptions/{subId}/cancelEDIT_BILLING_PROFILESCancel subscription
POST/billing-profiles/{id}/subscriptions/{subId}/reactivateEDIT_BILLING_PROFILESReactivate subscription
POST/billing-profiles/{id}/subscriptions/{subId}/retry-paymentEDIT_BILLING_PROFILESRetry failed payment

Payment Profiles

MethodPathPermissionDescription
GET/payment-profilesVIEW_PAYMENT_PROFILESList profiles
POST/payment-profilesCREATE_PAYMENT_PROFILESCreate profile
GET/payment-profiles/options--Profile options
GET/payment-profiles/{id}VIEW_PAYMENT_PROFILESShow profile
PUT/payment-profiles/{id}EDIT_PAYMENT_PROFILESUpdate name
DELETE/payment-profiles/{id}DELETE_PAYMENT_PROFILESDelete profile
POST/payment-profiles/{id}/defaultEDIT_PAYMENT_PROFILESSet as default
POST/payment-profiles/{id}/stripe/urlEDIT_PAYMENT_PROFILESCreate the Stripe onboarding link
PUT/payment-profiles/{id}/stripe/descriptorEDIT_PAYMENT_PROFILESUpdate the statement descriptor
PUT/payment-profiles/{id}/stripe/business-websiteEDIT_PAYMENT_PROFILESUpdate the Stripe business website
PUT/payment-profiles/{id}/stripe/payout-scheduleEDIT_PAYMENT_PROFILESUpdate the payout schedule
GET/payment-profiles/{id}/stripe/payoutsVIEW_PAYMENT_PROFILESList Stripe payouts
GET/payment-profiles/{id}/stripe/payouts/datatableVIEW_PAYMENT_PROFILESStripe payouts datatable
POST/payment-profiles/{id}/stripe/account-sessionVIEW_PAYMENT_PROFILESCreate an embedded Stripe account session
POST/payment-profiles/{id}/viva-wallet/accountEDIT_PAYMENT_PROFILESCreate the Viva Wallet connected account

(Verified: routes/api/backoffice/settings/payment-profiles.php:8-57 — 15 routes in the prefix.)

Branding Profiles

MethodPathPermissionDescription
GET/branding-profilesVIEW_BRANDING_PROFILEList profiles
GET/branding-profiles/options--Profile options
POST/branding-profilesCREATE_BRANDING_PROFILECreate profile
POST/branding-profiles/set-defaultEDIT_BRANDING_PROFILESet default profile
GET/branding-profiles/{id}VIEW_BRANDING_PROFILEShow profile
PUT/branding-profiles/{id}EDIT_BRANDING_PROFILEUpdate profile
PUT/branding-profiles/{id}/nameEDIT_BRANDING_PROFILEUpdate name only
DELETE/branding-profiles/{id}DELETE_BRANDING_PROFILEDelete profile

Languages & Translations

MethodPathPermissionDescription
GET/languagesVIEW_LANGUAGEList languages
GET/languages/check-incompleteVIEW_LANGUAGECheck incomplete translations
POST/languagesCREATE_LANGUAGEAdd language
POST/languages/set-default/{id}EDIT_LANGUAGESet default language
POST/languages/publish/{id}EDIT_LANGUAGEPublish language
POST/languages/unpublish/{id}EDIT_LANGUAGEUnpublish language
DELETE/languages/delete/{id}DELETE_LANGUAGEDelete language
GET/translationsVIEW_TRANSLATIONGet translations
POST/translationsEDIT_TRANSLATIONSave translations
GET/translations/datatableVIEW_TRANSLATIONTranslation datatable
POST/translations/translateEDIT_TRANSLATIONAuto-translate
GET/translations/bulk/{id}VIEW_TRANSLATIONShow bulk translations
POST/translations/bulk/{id}EDIT_TRANSLATIONBulk auto-translate

Receipts & Activity Logs

MethodPathPermissionDescription
GET/receipts/{locationId}--Get receipt settings
PUT/receipts/{locationId}EDIT_RECEIPTSUpdate receipt settings
GET/activity-logs/datatableVIEW_ACTIVITY_LOGSList activity logs

Online Ordering Endpoints

Public (No Auth)

MethodPathDescription
GET/online-ordering/address-suggestionsAutocomplete address search
GET/online-ordering/coordinates-to-addressReverse geocode coordinates
GET/online-ordering/{slug}Get restaurant/location info
POST/online-ordering/payment/processProcess Square payment

Restaurant Suggestions (Optional Auth)

MethodPathDescription
GET/online-ordering/restaurant-suggestions/nearbyGet nearby restaurants
POST/online-ordering/restaurant-suggestions/upvoteUpvote a restaurant
GET/online-ordering/restaurant-suggestions/topGet top suggestions
GET/online-ordering/restaurant-suggestions/categoriesGet suggestion categories

Order Flow (Optional Customer Auth)

MethodPathMiddlewareDescription
POST/online-ordering/{slug}/{locationId}/offers/validateauth.optional, tenant:online-orderingValidate offers
POST/online-ordering/{slug}/{locationId}/available-timeslotsauth.optional, tenant:online-orderingGet available timeslots
POST/online-ordering/{slug}/{locationId}/paymentauth.optional, tenant:online-orderingCreate payment
POST/online-ordering/{slug}/{locationId}/verify-paymentauth.optional, tenant:online-orderingVerify payment status

Customer Order History

MethodPathMiddlewareDescription
POST/customer/{slug}/order-historyauth.optional, throttle:5,1Get order history
GET/customer/{slug}/order-detail/{orderId}auth.optional, throttle:10,1Get order detail

Table QR Ordering (Optional Auth)

MethodPathDescription
GET/table-qr-ordering/{locationId}/{tableSectionId}/{tableNumber}Handle QR code scan
GET/table-qr-ordering/{locationId}/session/{sessionId}Get session details
POST/table-qr-ordering/{locationId}/session/{sessionId}/bind-orderBind order to session

BackOffice Online Ordering Management

MethodPathPermissionDescription
GET/online-ordering/{locationId}VIEW_ONLINE_ORDERINGShow settings
PUT/online-ordering/{locationId}EDIT_ONLINE_ORDERINGUpdate settings
POST/online-ordering/{locationId}/snoozeEDIT_ONLINE_ORDERINGSnooze ordering
GET/online-settings/{locationId}--Show online settings
PUT/online-settings/{locationId}--Update online settings
GET/in-house-settings/{locationId}VIEW_SALES_CHANNELShow in-house settings
PUT/in-house-settings/{locationId}EDIT_IN_HOUSE_CHANNELUpdate in-house settings

QR Ordering Management

MethodPathDescription
GET/qr-ordering/{locationId}Show QR ordering config
PUT/qr-ordering/{locationId}Update QR ordering config
GET/qr-ordering/{locationId}/qr-download-optionsGet QR download options
POST/qr-ordering/{locationId}/download-multipleDownload multiple QR codes

Table Sections

MethodPathDescription
POST/table-sectionsCreate table section
GET/table-sections/location/{locationId}List sections for location
GET/table-sections/{id}Show section
PUT/table-sections/{id}Update section
DELETE/table-sections/{id}Delete section

Kiosk Endpoints

All require auth + capacitor.auth + type:kiosk + tenant:kiosk middleware.

MethodPathDescription
POST/kiosk/payment/create-intentCreate payment intent on terminal
POST/kiosk/payment/send-receiptSend receipt to customer
POST/kiosk/payment/cancel-actionCancel terminal payment action
GET/kiosk/payment/session-statusCheck payment session status
GET/kiosk/payment/details/{idempotencyKey}Get payment details after completion
POST/kiosk/customer/identifyIdentify the customer at the kiosk
POST/kiosk/loyaltyLogin customer for loyalty
GET/kiosk/loyaltyGet loyalty details
GET/kiosk/offersList available offers
POST/kiosk/offers/validateValidate selected offers
POST/kiosk/gift-cards/validateValidate a gift-card code
POST/kiosk/pricing/quotePrice a basket before payment
GET/kiosk/updates/checkCheck for app updates
POST/kiosk/updates/fcm/registerRegister FCM token
POST/kiosk/test-print-receiptTest print a receipt
POST/kiosk/printers/discovery-reportReport LAN-discovered printers (throttle:printer-discovery); same body contract as the POS twin

There is no unauthenticated kiosk endpoint. In particular there is no /kiosk/latest-app-version and no /kiosk/download-apk route -- app updates go through GET /kiosk/updates/check, which is inside the authenticated group above.

(Verified: routes/api.php -- the whole /kiosk prefix group under ['capacitor.auth', 'type:kiosk', 'tenant:kiosk']; no other route file defines a /kiosk path.)


Kitchen Display System (KDS)

Requires auth + type:kds + tenant:kds middleware.

MethodPathDescription
POST/kds/fcm-tokenUpdate FCM token
GET/kds/ordersList current orders
PUT/kds/mark-modifierMark modifier item status
PUT/kds/mark-itemMark order item status
GET/kds/itemsList KDS items
PUT/kds/{id}/in-progressMark order in progress
PUT/kds/{id}/readyMark order ready
PUT/kds/{id}/completeMark order complete
PUT/kds/{id}/prioritizePrioritize order
PUT/kds/{id}/holdHold order
GET/kds/metrics/summaryKitchen metrics summary
PUT/kds/{id}/recallRecall a completed order
PUT/kds/{id}/releaseRelease a held order
PUT/kds/{id}/unprioritizeRemove priority from an order
POST/kds/customer-arrivedCustomer-arrival signal (order id in the body, not the path)
POST/kds/estimated-arrivalEstimated-arrival signal (order id in the body, not the path)
PUT/kds/settingsUpdate KDS settings
GET/kds/settingsRead the device's materialized settings
GET/kds/categoriesList KDS categories

POS Endpoints (First-Party Register)

Every route requires auth + capacitor.auth + type:pos + tenant:pos — the group is opened at routes/api/pos.php:17 and required from routes/api.php:239, which sits inside the top-level Route::middleware('auth')->group(…) at routes/api.php:115. The group carries nofirst-party-pos gate: a POS device JWT is the only thing needed, and that JWT can only exist for a merchant who already holds the pos_provider='upvendo' marker (POS device creation is gated in DeviceService::store()).

Routes marked pos.staff additionally require the staff PIN-session token in the X-Pos-Staff-Token header.

MethodPathExtra middlewareDescription
POST/pos/staff/pin-loginthrottle:pos-pinOpen a staff PIN session
POST/pos/staff/pin-logoutpos.staffClose the staff session
POST/pos/staff/escalatepos.staff, throttle:pos-escalateManager escalation → approval_token
GET/pos/bootstrap--Catalog + configuration bootstrap
POST/pos/refundspos.staffSigned cash refund of a settled sale
POST/pos/tillspos.staffOpen a till
GET/pos/tills/currentpos.staffCurrent till (bare resource; 204 when none)
POST/pos/tills/{tillId}/movementspos.staffTyped drawer movement
POST/pos/tills/{tillId}/endpos.staffStop taking cash (OPEN → ENDED)
POST/pos/tills/{tillId}/countpos.staffBlind count
POST/pos/tills/{tillId}/closepos.staffDeclare and freeze
GET/pos/business-daypos.staffPeek the fiscal business day
POST/pos/business-day/closepos.staffZ close
GET/pos/business-day/x-reportpos.staffMid-day X report
GET/pos/orderspos.staffList orders (state required: open|settling|settled)
PUT/pos/orders/{clientUuid}pos.staffUpsert an order (optimistic version; 409 on conflict)
GET/pos/orders/{clientUuid}pos.staffShow order
POST/pos/orders/{clientUuid}/firepos.staffFire to kitchen
POST/pos/orders/{clientUuid}/settlepos.staffSettle in cash
POST/pos/orders/{clientUuid}/voidpos.staffVoid an order
POST/pos/printers/discovery-reportthrottle:printer-discoveryReport LAN-discovered printers (device-level, no staff session)
GET/pos/printerspos.staffList the site's printers (optional ?state=unconfigured|paired)
POST/pos/printers/{id}/configurepos.staffWrite a printer's configuration
POST/pos/printers/{id}/pairing-sessionpos.staffStart pairing
GET/pos/printers/{id}/pairing-sessionpos.staffPairing status
POST/pos/printers/{id}/pairing-session/confirm-writepos.staffConfirm the write
POST/pos/printers/{id}/test-printpos.staffMerchant-facing test print (no manager escalation)
GET/pos/kds-stationspos.staffStation picker (location comes from the device token)
GET/pos/channel-orderspos.staffRead-only omni-channel order stream
GET/pos/channel-orders/{id}pos.staffShow a channel order

The printer routes split across three authorization levels, and the middleware column does not tell the whole story -- PrinterPairingOrchestrator re-derives the required principal from what is authenticated rather than from which mount ran (PrinterPairingOrchestrator.php:351-486):

LevelRoutesRequirement
Devicediscovery-reportDevice JWT only; runs at boot before anyone signs in
OperatorGET /printers, GET …/pairing-session, …/confirm-write, …/test-printStaff PIN session; the device must be type:pos (a kiosk/KDS token is refused)
Mint…/configure, …/pairing-session (POST)Staff PIN session plus manager approval -- self-permitting if the signed-in member holds approve_as_manager, otherwise a short-lived approval_token from POST /pos/staff/escalate. These mint or disclose the poll credential

(Verified: routes/api/pos.php:21-121.)

Discovery-report body contract

Shared by POST /pos/printers/discovery-report and POST /kiosk/printers/discovery-report -- one DiscoveryReportRequest, one service, so a printer seen by either device is one record.

json
{
  "printers": [
    {
      "mac": "00:26:AB:11:22:33",
      "ip": "192.168.1.40",
      "model": "TM-m30III",
      "device_name": "Kitchen",
      "serial": "X3AB1234567",
      "paper_width_mm": 80,
      "mac_source": "ipp-uuid"
    }
  ]
}
  • printers is required, min:1, max:16 (PrinterProvisioningService::MAX_BATCH); an oversized batch is rejected, never truncated.
  • Each entry is a strict key allow-list -- array:mac,ip,model,device_name,serial,paper_width_mm,mac_source (DiscoveryReportRequest.php:54, :72). Any other key 422s the whole batch with a message naming the accepted keys.
  • mac is the only required key (:73, App\Rules\MacAddress). ip must pass ip; paper_width_mm must be 58 or 80; model / device_name / serial are max:64.
  • mac_source is nullable + in:ipp-uuid,star-http,manual (:82, App\Enums\PrinterMacSource). Optional on purpose -- requiring it would 422 every fleet build that predates the field. An omitted value is written as ipp-uuid on the write path only (PrinterProvisioningService.php:581-584).
  • There is deliberately no location_id, vendor_id, device_id or token in the body: the scope of a report is the reporting device, derived server-side (DiscoveryReportRequest.php:11-45).
  • Response: {"reported": n, "known": n} — counts only, deliberately no per-MAC detail, so the response cannot be used to probe whether a MAC exists on another merchant (PrinterDiscoveryController.php:36-39).
  • throttle:printer-discovery = 4/min and 60/hour per device credential, plus 120/min per client address resolved through ClientIpHelper (AppServiceProvider.php:291-303).

Printer Poll Endpoint

MethodPathMiddlewareDescription
POST/print/epson/sdpthrottle:printer-poll, printer.authEpson Server Direct Print: the printer asks for work and reports results

Mounted outside every session/JWT/tenant group (routes/api/print.php:39-48) — a printer has no user and no JWT, only a device token, and that token selects the tenant. It rides either as ?token= or as the X-Printer-Token header. Star CloudPRNT is a planned sibling route ("Wave 2") and is not shipped.


Integration Endpoints (BackOffice)

Every integration prefix carries admin-vendor-override, which lets an authorised internal admin or the merchant's own reseller act on a merchant by passing ?vendor_id (a no-op when it is absent). Every prefix that is keyed by {locationId} additionally carries location-ownerafter it -- the cross-tenant guard that rejects a {locationId} belonging to another merchant. Mutating routes are further gated by permission:MANAGE_INTEGRATION_SETTINGS; read routes and sync triggers generally are not.

(Verified: the ->middleware([...]) argument on the prefix group of each file under routes/api/backoffice/; guard behaviour in app/Http/Middleware/EnsureLocationBelongsToMerchant.php.)

Auto-sync time payload (sync_time + sync_timezone)

Five back-office endpoints write the daily auto-sync schedule. They share one payload contract, enforced by five FormRequests with identical rules:

EndpointFormRequestsync_timeScope
POST /square/sync-timeBackOffice/SquareIntegration/UpdateSyncTimeRequestrequiredmerchant
PUT /lightspeed/sync-timeBackOffice/Lightspeed/UpdateSyncTimeRequestnullablemerchant
POST /mpluskassa (initiate/settings save)BackOffice/MplusKassa/InitiateMplusKassaRequestnullablemerchant
POST /kassanet/{provider}/{locationId} (initiate/settings save)BackOffice/Kassanet/InitiateKassanetRequestnullablelocation
KassanetIntegrationController::updateAutoSyncScheduleBackOffice/Kassanet/UpdateAutoSyncScheduleRequestnullablelocation

The last row has no registered routeroutes/api/backoffice/kassanet.php declares only GET, POST, POST /sync-menu, GET /test and DELETE. Hendrickx/Vanhoutte sync times are written through POST /kassanet/{provider}/{locationId} (initiate), which accepts the same keys.

KeyRulesNotes
sync_timestring, regex:/^([01]?[0-9]|2[0-3]):(00|30)$/Minutes restricted to 00/30. Stored verbatim as a local wall-clock in settings.sync_time — it is not converted to UTC. Sending null clears it (disables auto-sync) except on Square, where the key is required.
sync_timezonenullable, string, timezoneIANA identifier, validated by Laravel's timezone rule (PHP's timezone_identifiers_list()). Optional on all five.
enable_auto_syncsometimes, booleanSquare only.

sync_timezone resolution on write is SyncScheduleResolver::resolveTimezone($request ?? $stored ?? $locationDefault), so omitting the key preserves the stored zone rather than overwriting it. The default (first write only) is the integration's own location zone for the location-scoped Kassanet providers, otherwise the first merchant location that has a zone, falling back to the platform constant SyncScheduleResolver::DEFAULT_TIMEZONE = 'Europe/Brussels'. The zone is only written alongside a non-empty sync_time; clearing the time leaves the stored zone untouched. Every write also resets settings.next_run_time to null so the scheduler recomputes the firing instant.

The resolved zone is returned for display only on each integration's status/show payload as sync_timezone (never nullThirdPartyIntegration::getSyncTimezone() substitutes the default). The back-office does not send it: SyncTimeField.vue renders it as a read-only caption ("Runs in the {timezone} timezone") under the time picker and omits the caption entirely when the API supplies no zone.

Why local wall-clock: SyncScheduleResolver::nextRunAfter() converts sync_time + sync_timezone to the next UTC instant strictly after "now", once, in the scheduler. A UTC-stored 03:30 drifts to 04:30 local at every DST transition; a local-stored 03:30 does not. InHouseAutoSyncCommand therefore selects work by settings.next_run_time <= now (or unset) instead of bucketing the current minute — a +05:45 zone puts a run at :45, inside no :00/:30 bucket.

Pre-existing rows: the one-shot data-repair:sync-timezone-backfill command reinterpreted every sync_time that had no sync_timezone from UTC into the resolved local zone, preserving the firing instant as of the day it ran, snapped the result to the nearest :00/:30 (logging every snap, which can shift the instant by up to 15 minutes), and nulled next_run_time. It skips any integration that already carries sync_timezone, so it is idempotent.

(Verified: app/Services/BackOffice/SyncSchedule/SyncScheduleResolver.php:19-137; app/RawModels/ThirdPartyIntegration.php:139-148; app/Http/Requests/BackOffice/SquareIntegration/UpdateSyncTimeRequest.php:19-23; app/Http/Requests/BackOffice/Lightspeed/UpdateSyncTimeRequest.php:12-24; app/Http/Requests/BackOffice/MplusKassa/InitiateMplusKassaRequest.php:17-26; app/Http/Requests/BackOffice/Kassanet/InitiateKassanetRequest.php:25-34; app/Http/Requests/BackOffice/Kassanet/UpdateAutoSyncScheduleRequest.php:22-31; app/Console/Commands/InHouseAutoSyncCommand.php:322-352, 435-457; app/Console/Commands/DataRepair/SyncTimezoneBackfill.php:32-130; upvendo-backoffice src/components/SyncTimeField.vue:4-18, 54-60. Upstream: upvendo-backend PR #1618, upvendo-backoffice PR #2462.)

Deliveroo

Prefix /deliveroo/{locationId}, middleware admin-vendor-override + location-owner.

MethodPathPermissionDescription
GET/deliveroo/{locationId}--Get integration status
POST/deliveroo/{locationId}MANAGE_INTEGRATION_SETTINGSEnable integration
PUT/deliveroo/{locationId}MANAGE_INTEGRATION_SETTINGSUpdate integration
DELETE/deliveroo/{locationId}MANAGE_INTEGRATION_SETTINGSDisable integration
POST/deliveroo/{locationId}/sync-menuMANAGE_INTEGRATION_SETTINGSSync menu to Deliveroo
POST/deliveroo/{locationId}/sync-availabilityMANAGE_INTEGRATION_SETTINGSPush item availability to Deliveroo
GET/deliveroo/{locationId}/logs--Integration log tail (query limit, default 50, clamped 1-200)
GET/deliveroo/{locationId}/site-status--Read the Deliveroo site's open/closed state
PUT/deliveroo/{locationId}/site-statusMANAGE_INTEGRATION_SETTINGSSet site status; body {status} validated in:OPEN,CLOSED,READY_TO_OPEN

(Verified: routes/api/backoffice/deliveroo.php -- the complete file.)

Uber Eats

Prefix /uber-eats/{locationId}, middleware admin-vendor-override + location-owner.

There is no PUT /uber-eats/{locationId}. The only PUT verbs on this prefix are /settings, /store/status, /store/prep-time and /store/fulfillment. Settings are updated through PUT /uber-eats/{locationId}/settings.

MethodPathPermissionDescription
GET/uber-eats/{locationId}--Get integration status
POST/uber-eats/{locationId}MANAGE_INTEGRATION_SETTINGSEnable integration
DELETE/uber-eats/{locationId}MANAGE_INTEGRATION_SETTINGSDisable integration
POST/uber-eats/{locationId}/select-storeMANAGE_INTEGRATION_SETTINGSBind the integration to an Uber Eats store
GET/uber-eats/{locationId}/store-list--List Uber Eats stores
GET/uber-eats/{locationId}/configuration--Get integration configuration
PUT/uber-eats/{locationId}/settingsMANAGE_INTEGRATION_SETTINGSUpdate integration settings
POST/uber-eats/{locationId}/sync-menu--Sync menu to Uber Eats
GET/uber-eats/{locationId}/store--Get store details
GET/uber-eats/{locationId}/store/status--Get store online/offline status
PUT/uber-eats/{locationId}/store/statusMANAGE_INTEGRATION_SETTINGSSet store online/offline
PUT/uber-eats/{locationId}/store/prep-timeMANAGE_INTEGRATION_SETTINGSUpdate prep time
PUT/uber-eats/{locationId}/store/fulfillmentMANAGE_INTEGRATION_SETTINGSUpdate fulfillment settings
GET/uber-eats/{locationId}/menu--Read the remote (Uber Eats) menu
PATCH/uber-eats/{locationId}/menu/items/{itemId}--Update a remote menu item
GET/uber-eats/{locationId}/orders--List Uber Eats orders
GET/uber-eats/{locationId}/orders/{orderId}--Show one Uber Eats order
POST/uber-eats/{locationId}/orders/{orderId}/{action}--Act on an order (see allowed actions below)
GET/uber-eats/{locationId}/promotions--List promotions
POST/uber-eats/{locationId}/promotionsMANAGE_INTEGRATION_SETTINGSCreate a promotion
GET/uber-eats/{locationId}/promotions/{promotionId}--Show a promotion
DELETE/uber-eats/{locationId}/promotions/{promotionId}MANAGE_INTEGRATION_SETTINGSRevoke a promotion
POST/uber-eats/{locationId}/reportsMANAGE_INTEGRATION_SETTINGSRequest a report
GET/uber-eats/{locationId}/reports/{reportId}--Get report status/metadata
GET/uber-eats/{locationId}/reports/{reportId}/download--Download a report

{action} on the order-action route is constrained by whereIn to exactly: accept, deny, cancel, ready, ready-time, adjust-price, validate-fulfillment, resolve-fulfillment, replacement-recommendations, courier-count, merchant-delivery-status. Any other value 404s at the router.

(Verified: routes/api/backoffice/uber-eats.php -- the complete file, including the whereIn action list.)

Square

Prefix /square, middleware admin-vendor-override. Merchant-level -- not keyed by location.

MethodPathDescription
GET/square/statusGet integration status
POST/square/oauthInitiate OAuth flow
POST/square/sync-timeUpdate sync schedule (sync_time required, sync_timezone and enable_auto_sync optional -- see Auto-sync time payload)
POST/square/disableDisable integration
POST/square/start-syncStart manual sync
GET/square/testTest connection

(Verified: routes/api/backoffice/square.php -- the complete file.)

Shopify

MethodPathDescription
GET/shopify/{locationId}Get integration status
PUT/shopify/{locationId}Update integration
DELETE/shopify/{locationId}Disable integration
POST/shopify/{locationId}/sync-menuExport menu to Shopify
POST/shopify/{locationId}/import-menuImport menu from Shopify
POST/shopify/{locationId}/oauthInitiate OAuth flow

(Verified: routes/api/backoffice/shopify.php -- the complete file. Prefix middleware admin-vendor-override + location-owner; every mutating route carries permission:MANAGE_INTEGRATION_SETTINGS.)

Kassanet (POS Integration)

Prefix /kassanet/{provider}/{locationId}, middleware admin-vendor-override + location-owner.

MethodPathDescription
GET/kassanet/{provider}/{locationId}Get integration status
POST/kassanet/{provider}/{locationId}Initiate integration / save settings (also carries sync_time + optional sync_timezone -- see Auto-sync time payload)
POST/kassanet/{provider}/{locationId}/sync-menuSync menu
GET/kassanet/{provider}/{locationId}/testTest connection
DELETE/kassanet/{provider}/{locationId}Delete integration

(Verified: routes/api/backoffice/kassanet.php -- the complete file.)

Lightspeed (K-Series)

Prefix /lightspeed, middleware admin-vendor-override. Merchant-level -- not keyed by location, so there is no location-owner on the group; per-location operations take {upvendoLocationId} in the path instead. The prefix dispatches internally to the K-Series implementation.

MethodPathPermissionDescription
GET/lightspeed/status--Get integration status
POST/lightspeed/oauthMANAGE_INTEGRATION_SETTINGSInitiate OAuth flow
POST/lightspeed/disableMANAGE_INTEGRATION_SETTINGSDisable integration
GET/lightspeed/test--Test connection
GET/lightspeed/mappings--List location mappings
PUT/lightspeed/mappings/{upvendoLocationId}MANAGE_INTEGRATION_SETTINGSCreate/update a location mapping
DELETE/lightspeed/mappings/{upvendoLocationId}MANAGE_INTEGRATION_SETTINGSRemove a location mapping
POST/lightspeed/mappings/{upvendoLocationId}/pauseMANAGE_INTEGRATION_SETTINGSPause a mapping
POST/lightspeed/mappings/{upvendoLocationId}/resumeMANAGE_INTEGRATION_SETTINGSResume a mapping
POST/lightspeed/mappings/{upvendoLocationId}/syncMANAGE_INTEGRATION_SETTINGSSync a mapping
PUT/lightspeed/mappings/{upvendoLocationId}/order-settingsMANAGE_INTEGRATION_SETTINGSPer-location account-profile (order-settings) overrides
PUT/lightspeed/payment-methodsMANAGE_INTEGRATION_SETTINGSMerchant-level payment-method to K-Series tender map
PUT/lightspeed/sync-timeMANAGE_INTEGRATION_SETTINGSMerchant-level scheduled auto-sync time (sync_time, optional sync_timezone -- see Auto-sync time payload)
GET/lightspeed/account-profiles--List K-Series account (order) profiles
GET/lightspeed/account-profiles/tax-preview--Read-only per-profile VAT preview
GET/lightspeed/floorplans--List floorplans
GET/lightspeed/floorplans/{floorplanId}/tables--List tables on a floorplan
GET/lightspeed/menus--List remote menus
GET/lightspeed/menus/{menuId}--Load one remote menu
GET/lightspeed/menu-modifiers--List remote menu modifiers
GET/lightspeed/menu-discounts--List remote menu discounts
POST/lightspeed/menu-suggestion/{upvendoLocationId}/adoptMANAGE_INTEGRATION_SETTINGSCopy the suggested menu into a merchant-owned menu
POST/lightspeed/menu-suggestion/{upvendoLocationId}/dismissMANAGE_INTEGRATION_SETTINGSStop suggesting the menu
POST/lightspeed/menu-suggestion/{upvendoLocationId}/useMANAGE_INTEGRATION_SETTINGSIdempotent adopt + publish + visibility + location default
GET/lightspeed/menu-suggestion/{upvendoLocationId}/diff--Non-destructive diff for an adopted menu
GET/lightspeed/item-availability--Read POS item availability
POST/lightspeed/orders/localMANAGE_INTEGRATION_SETTINGSPush a local (eat-in) order to the POS
POST/lightspeed/orders/to-goMANAGE_INTEGRATION_SETTINGSPush a to-go order to the POS
GET/lightspeed/checks--List open checks
GET/lightspeed/checks/table/{tableNumber}--Get the open check for a table
POST/lightspeed/payMANAGE_INTEGRATION_SETTINGSApply a payment to a check
GET/lightspeed/items--List items
GET/lightspeed/rich-items--List items with enriched attributes
GET/lightspeed/allergens--List allergens
GET/lightspeed/financial/businesses--List financial businesses
GET/lightspeed/financial/tax-rates--List tax rates
GET/lightspeed/financial/payment-methods--List payment methods
GET/lightspeed/financial/accounting-groups--List accounting groups
GET/lightspeed/financial/daily--Daily financials

The two order-push routes are gated by MANAGE_INTEGRATION_SETTINGS rather than a dedicated order permission: CREATE_ORDERS / PROCESS_ORDERS are currently unwired (assigned to no role), so they would fail closed for everyone.

(Verified: routes/api/backoffice/lightspeed.php -- the complete file, including the in-file comments explaining the admin-vendor-override choice and the order-permission fallback.)

MplusKassa

Prefix /mpluskassa, middleware admin-vendor-override. Merchant-level -- not keyed by location.

MethodPathPermissionDescription
POST/mpluskassaMANAGE_INTEGRATION_SETTINGSInitiate integration / save settings (also carries sync_time + optional sync_timezone -- see Auto-sync time payload)
GET/mpluskassa/status--Get integration status
POST/mpluskassa/disableMANAGE_INTEGRATION_SETTINGSDisable integration
POST/mpluskassa/start-syncMANAGE_INTEGRATION_SETTINGSStart a manual sync
GET/mpluskassa/test--Test connection
GET/mpluskassa/healthcheck--Integration health check
GET/mpluskassa/branches--List MplusKassa branches
POST/mpluskassa/locations/syncMANAGE_INTEGRATION_SETTINGSSync locations
GET/mpluskassa/location-mappings--Get location mappings
PUT/mpluskassa/location-mappingsMANAGE_INTEGRATION_SETTINGSUpdate location mappings
GET/mpluskassa/diagnose--Diagnostics
POST/mpluskassa/probe-capabilitiesMANAGE_INTEGRATION_SETTINGSProbe API capabilities
POST/mpluskassa/toggle-live-inventoryMANAGE_INTEGRATION_SETTINGSToggle live POS inventory
POST/mpluskassa/inventory/live-refresh--Refresh live inventory
GET/mpluskassa/relations--List POS relations (customers)
POST/mpluskassa/relations/sync--Sync relations
POST/mpluskassa/relations/link--Link a relation to an Upvendo customer
POST/mpluskassa/relations/create--Create a relation
POST/mpluskassa/relations/{relationNumber}/pricing-check--Pricing check for one relation
POST/mpluskassa/pricing/quote--Pricing quote (determinePricing)

(Verified: routes/api/backoffice/mpluskassa.php -- the complete file.)

ShopCaisse

Two prefixes. /shopcaisse is merchant-level with admin-vendor-override; /shopcaisse/{locationId} adds location-owner.

MethodPathPermissionDescription
GET/shopcaisse/status--Get integration status
POST/shopcaisse/locations/sync--Sync locations
POST/shopcaisse/test--Test the merchant-level connection
POST/shopcaisse/sync--Sync at merchant level
GET/shopcaisse/{locationId}--Show the location's integration
POST/shopcaisse/{locationId}/enableMANAGE_INTEGRATION_SETTINGSEnable integration for the location
PUT/shopcaisse/{locationId}/tokenMANAGE_INTEGRATION_SETTINGSRotate the API token
POST/shopcaisse/{locationId}/sync--Sync the location
POST/shopcaisse/{locationId}/test--Test the location's connection
POST/shopcaisse/{locationId}/orders/verify--Verify an order against the POS
GET/shopcaisse/{locationId}/stores--List ShopCaisse stores
POST/shopcaisse/{locationId}/stores/selectMANAGE_INTEGRATION_SETTINGSBind the integration to a store
DELETE/shopcaisse/{locationId}MANAGE_INTEGRATION_SETTINGSDelete the integration

(Verified: routes/api/backoffice/shopcaisse.php -- the complete file, both prefix groups.)

Ordering Channels

MethodPathDescription
GET/in-house-channels/{locationId}List in-house channels
GET/online-channels/{locationId}List online channels

(Verified: routes/api/backoffice/ordering-channels.php -- the complete file.)


Webhook Endpoints

All webhook endpoints are unauthenticated (guest routes) but use signature verification middleware.

MethodPathMiddlewareDescription
POST/stripe-webhook/{countryCode}--Stripe webhook handler
GET/viva-webhook/{countryCode}/{eventTypeId}--Viva Wallet verification handshake
POST/viva-webhook/{countryCode}/{eventTypeId}verify.viva-webhookViva Wallet events (IP-allowlisted)
POST/shopify-webhookverify.shopify-webhookShopify webhook handler
POST/webhook/deliveroo/ordersverify.deliveroo-webhookDeliveroo order events
POST/webhook/deliveroo/menuverify.deliveroo-webhookDeliveroo menu events
POST/webhook/uber-eatsverify.uber-eats-webhookUber Eats events
POST/webhook/squareverify.square-webhookSquare events
POST/webhook/shopcaisseverify.shopcaisse-webhookShopCaisse events
POST/webhook/mpluskassa/{event}verify.mpluskassa-webhookMplusKassa events (event type in the path)
POST/webhook/lightspeedverify.lightspeed-k-series-webhookLightspeed K-Series events
POST/crm/intakeverify.crm-webhook, throttle:60,1Public CRM form intake (queues an Odoo sync)

(Verified: routes/api/guest.php -- the /webhook prefix group and the top-level stripe-webhook / viva-webhook / shopify-webhook / crm/intake routes.)

OAuth Callbacks

MethodPathDescription
GET/shopify-callbackShopify OAuth callback
GET/square-callbackSquare OAuth callback
GET/uber-eats/callbackUber Eats OAuth callback
GET/lightspeed/callbackLightspeed K-Series OAuth callback
GET/oauth/lightspeed/callbackLightspeed K-Series OAuth callback (alternate path, same handler)

Both Lightspeed paths resolve to ThirdPartyAuthController::handleLightspeedKSeriesCallback; they exist so a redirect URI registered under either shape keeps working.

(Verified: routes/api/guest.php -- the four ThirdPartyAuthController callback routes and their named routes lightspeed.callback / oauth.lightspeed.callback.)


Utility Endpoints

AI Photo (Authenticated BackOffice)

The AI photo endpoints live under /back-office/ai-photo. There is no /photo-studio API prefix -- "Photo Studio" is the name of the back-office page (/settings/photo-studio) and of the storage folder, not of a route. A stale docs/PHOTO_PROCESSING.md in the backend repo still describes POST /api/photo-studio/remove-background and friends; those routes do not exist.

MethodPathDescription
POST/ai-photo/generateGenerate an AI photo
GET/ai-photo/status/{contentId}Poll generation status
GET/ai-photo/creditsGet remaining credits
GET/ai-photo/credits-package-optionsList credit packages
POST/ai-photo/create-checkout-urlCreate a Stripe checkout URL for credits
POST/ai-photo/purchase-with-defaultPurchase credits with the default payment method

(Verified: routes/api/backoffice/ai-photo.php -- the complete file. No route file anywhere under routes/ defines a photo-studio path.)

Guided Setup Management

Prefix /back-office/guided-setup-management. The lookup route is available to any authenticated back-office user (Emily uses it); everything else requires global-admin.

MethodPathMiddlewareDescription
GET/guided-setup-management/video/urlauthGet a setup video by URL (Emily integration)
GET/guided-setup-management/tasksauth, global-adminList all tasks with video info
POST/guided-setup-management/videoauth, global-adminCreate/update a video setting
GET/guided-setup-management/video/{taskId}auth, global-adminGet a task's video setting
DELETE/guided-setup-management/video/{taskId}auth, global-adminDelete a task's video setting

There is no /guided-setup prefix and no {service}-keyed setup route on production.

(Verified: routes/api/backoffice/guided-setup-management.php -- the complete file, both prefix groups.)

Setup Status

MethodPathDescription
GET/setup-statusMerchant setup completion status
GET/payment-onboarding-statusPayment onboarding status

(Verified: routes/api/backoffice/setup-status.php -- the complete file.)

Description Generator

MethodPathDescription
POST/description-generator/generateAI-generate item description

Loyalty (BackOffice)

MethodPathPermissionDescription
GET/loyalty/{locationId}VIEW_LOYALTYGet loyalty config
GET/loyalty/{locationId}/subscriptionEDIT_LOYALTYGet subscription info
POST/loyalty/{locationId}EDIT_LOYALTYCreate/update loyalty
DELETE/loyalty/{locationId}DELETE_LOYALTYDelete loyalty

(Verified: routes/api/backoffice/loyalty.php -- the complete file.)

Other

MethodPathDescription
POST/client-loggerClient-side error logging (capacitor.auth; top-level, not under /back-office). Watchlisted events are also counted per device in a time-bucketed cache key, and crossing the threshold posts once to a separate Slack webhook — edge-triggered, so a wedged terminal produces one message per window rather than one per failure. The main client-logger channel is a firehose, in which a device failing the same way repeatedly reads as normal traffic. Leaving the burst webhook unset disables the whole path.
GET/back-office/visibility-optionsChannel visibility options
GET/back-office/item-pricing-optionsItem pricing type options
GET/back-office/language-optionsLanguage select options
GET/back-office/featuresFeature flags for the current merchant

The super-admin middleware alias is registered in bootstrap/app.php but is not applied to any route on production. There is no /super-admin/* endpoint -- in particular no kiosk APK upload route. Global-administrator surfaces use the global-admin alias instead.

(Verified: routes/api.php for /client-logger and the three option endpoints inside the /back-office group; routes/api/backoffice/features.php; bootstrap/app.php for the unused super-admin alias.)