Appearance
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 filesroutes/web.php- Non-/apiweb routes:/health, the public/versioncommit-hash endpoint, the receipt view, and (test envs only) the monitoring dashboardroutes/api/guest.php- Unauthenticated routes (login, webhooks, OAuth callbacks, device activation)routes/api/field.php- Field Ops app routes (FieldSession audience,/fieldprefix)routes/api/backoffice/*.php- BackOffice CRUD routes (auto-loaded via glob)routes/api/backoffice/settings/*.php- Settings sub-routes (auto-loaded via glob fromsettings.php)routes/api/reseller/commissions.php- Reseller commission routes (auth:sanctum)routes/api/pos.php- First-party POS device routes (/posprefix, POS device JWT)routes/api/print.php- Thermal-printer poll route (/printprefix), mounted outside every auth grouproutes/api/dev/*.php- Dev/diagnostic routes for Lightspeed, MplusKassa, ShopCaisse and print. Test environments only, and additionally gated bye2e.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.phpis required atroutes/api.php:239andapp/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 exceptPOST /pos/select-provideradditionally carriesfirst-party-pos(EnsureFirstPartyPos), which 403s any merchant whosepos_provideris notupvendo— and in production only test-flagged merchants can currently become one (upvendois staged'test_only' => true,config/pos-providers.php:52). These endpoints are staged, not generally available.
Public Endpoints (No Auth Required)
Constants
| Method | Path | Description |
|---|---|---|
| GET | /country-options/{forPaymentProfile?} | List available countries (parameter is optional) |
| GET | /lang-options | List available languages |
| GET | /calling-code-options | List phone calling codes |
| GET | /branding-font-options | List branding font choices |
| GET | /allergens-options | List allergen options |
| GET | /dietary-preferences-options | List dietary preference options |
| GET | /dietary-supplements-options | List dietary supplement options |
| GET | /tags-options | List tag options |
Health Check
| Method | Path | Description |
|---|---|---|
| GET | /health | Simple health check |
| GET | /health/readiness | Readiness probe |
| GET | /health/detailed | Detailed health check with component status |
| GET | /health/fresh | Fresh health check (no cache) |
| GET | /health/status | Health status summary |
(Verified: routes/api/guest.php -- the HealthController block inside the guest group.)
Service Version
| Method | Path | Description |
|---|---|---|
| GET | /version | Commit 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/branchfall back to the literal string"unknown"when the commit cannot be resolved;deployed_atisnullin 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, thenpacked-refs), then"unknown".gitis never shelled out to, and nothing touches the database. - A detached
HEADyields the sha withbranch="unknown". environmentisapp()->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)
| Method | Path | Description |
|---|---|---|
| POST | /login | Login with email/password, returns JWT token (or verification_required if the email is unverified) |
| POST | /login/google | Sign in with a Google Identity Services credential (throttle 5/min) |
| POST | /back-office/register | Register new merchant account — returns verification_required, no token |
| POST | /back-office/verify-email | Consume the emailed verification token ({ token }) |
| POST | /back-office/resend-verification | Re-send the verification email ({ email }) |
| POST | /back-office/forget-password | Request password reset email |
| PUT | /back-office/update-password | Update password using reset token |
| POST | /back-office/request-otp | Request OTP code for two-factor auth |
| POST | /back-office/verify-otp | Verify OTP code and complete login |
| POST | /back-office/passkeys | Get WebAuthn challenge for passkey login |
| POST | /back-office/authenticate-passkey | Authenticate using WebAuthn passkey |
| GET | /back-office/join-merchant | Get details for merchant invitation |
| POST | /back-office/join-merchant | Accept merchant invitation |
BackOffice Authenticated User
| Method | Path | Middleware | Description |
|---|---|---|---|
| POST | /logout | auth, type:backoffice | Logout (client discards JWT) |
| POST | /back-office/start-using | auth, type:backoffice | Mark merchant as started |
| GET | /back-office/user | auth, type:backoffice | Get current user profile |
| PUT | /back-office/user-personal-info | auth, type:backoffice | Update personal info |
| PUT | /back-office/user-business-info | auth, type:backoffice | Update business info |
| GET | /back-office/merchant-options | auth, type:backoffice | List user's merchants |
| POST | /back-office/set-merchant | auth, type:backoffice | Switch active merchant (returns new JWT) |
| GET | /back-office/bootstrap | auth, type:backoffice | Single-request boot payload for the back-office SPA |
| GET | /validate-token | auth | Validate 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)
| Method | Path | Description |
|---|---|---|
| GET | /back-office/passkeys | Get user's registered passkeys |
| GET | /back-office/passkeys/setup | Get passkey registration options |
| POST | /back-office/passkeys/setup | Register a new passkey |
| DELETE | /back-office/passkeys | Delete a passkey |
Customer Authentication (Guest Routes)
| Method | Path | Description |
|---|---|---|
| POST | /customer/send-otp | Send 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/login | Verify OTP and login customer |
| POST | /customer/login/google | Sign in with a Google ID token (credential, optional location_id) |
The whole
/customerguest prefix carriesthrottle:20,1(20 req/min per IP). Both OTP routes are unauthenticated and abuse-prone —send-otpcreates a Customer and sends an SMS/email per call, andloginhas no per-attempt counter, so a 6-digit OTP would be grindable for its full 15-minute validity.
Customer Authenticated
| Method | Path | Middleware | Description |
|---|---|---|---|
| GET | /customer/user | auth, type:customer | Get customer profile |
| POST | /customer/logout | auth, type:customer | Logout customer |
| GET | /customer/personal-info | auth, type:customer | Get personal info |
| POST | /customer/personal-info | auth, type:customer | Update personal info |
| GET | /customer/addresses | auth, type:customer | List addresses |
| GET | /customer/addresses/options | auth, type:customer | Address form options |
| POST | /customer/addresses | auth, type:customer | Add new address |
| DELETE | /customer/addresses/{addressId} | auth, type:customer | Delete address |
| GET | /customer/loyalties/{slug} | auth, type:customer | List loyalty programs |
| GET | /customer/loyalties/{slug}/{locationId} | auth, type:customer | Loyalty program detail |
| GET | /customer/{slug}/locations | auth, type:customer | List user's locations |
Device Authentication (Guest)
| Method | Path | Middleware | Description |
|---|---|---|---|
| POST | /device-auth/activate | -- | Activate device with activation code |
| GET | /device-auth/admin-options | verify.admin-switch-ip | List switch targets for an on-site admin |
| POST | /device-auth/admin-switch | verify.admin-switch-ip | Switch the device to another target |
Device Authenticated
| Method | Path | Middleware | Description |
|---|---|---|---|
| GET | /device-auth/user | auth, tenant:kiosk,kds | Get device info |
| POST | /device-auth/network-info | auth, tenant:kiosk,kds | Update device network info |
| POST | /device-auth/heartbeat | auth, tenant:kiosk,kds | Device heartbeat / liveness ping |
| POST | /device-auth/logout | auth | Logout 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
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /items/datatable | VIEW_ITEMS | List items (DataTable format) |
| GET | /items/export | EXPORT_ITEMS | Export items to spreadsheet |
| GET | /items/options | -- | Item select options |
| POST | /items | CREATE_ITEMS | Create item |
| GET | /items/{id} | VIEW_ITEMS | Show item detail |
| PUT | /items/{id} | EDIT_ITEMS | Update item |
| DELETE | /items/{id} | DELETE_ITEMS | Delete item |
Categories
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /categories/datatable | VIEW_CATEGORIES | List categories |
| GET | /categories/options | -- | Category select options |
| POST | /categories | CREATE_CATEGORIES | Create category |
| GET | /categories/{id} | VIEW_CATEGORIES | Show category |
| PUT | /categories/{id} | EDIT_CATEGORIES | Update category |
| DELETE | /categories/{id} | DELETE_CATEGORIES | Delete category |
Menus
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /menu-options | -- | Menu select options |
| GET | /menus | VIEW_MENUS | List all menus |
| POST | /menus | CREATE_MENUS | Create menu |
| GET | /menus/{id} | VIEW_MENUS | Show menu detail |
| PUT | /menus/{id} | EDIT_MENUS | Update menu |
| DELETE | /menus/{id} | DELETE_MENUS | Delete menu |
| POST | /menus/duplicate/{id} | CREATE_MENUS | Duplicate menu |
| POST | /menus/draft/{id} | EDIT_MENUS | Set menu to draft |
| POST | /menus/archive/{id} | EDIT_MENUS | Archive menu |
| POST | /menus/publish/{id} | EDIT_MENUS | Publish menu |
| GET | /menus/display-group-items/{id} | VIEW_MENUS | List display group items |
| POST | /menus/display-group-items/{id} | EDIT_MENUS | Save display group items |
| DELETE | /menus/display-groups/{id} | EDIT_MENUS | Delete display group |
| POST | /menus/move-item-to-other-group/{id} | EDIT_MENUS | Move item between groups |
| POST | /menus/add-item-to-groups/{itemId} | EDIT_MENUS | Append an item to one or more display groups (does not remove it from its current group) |
| POST | /menus/display-group-items/{id}/draft | EDIT_MENUS | Save display-group items as a draft |
| POST | /menus/display-group-items/{id}/discard | EDIT_MENUS | Discard the display-group-items draft |
Modifier Groups
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /modifiers | -- | List all modifiers |
| GET | /modifier-groups | VIEW_MODIFIER_GROUPS | List modifier groups |
| POST | /modifier-groups | CREATE_MODIFIER_GROUPS | Create modifier group |
| GET | /modifier-groups/options | -- | Modifier group options |
| PUT | /modifier-groups/reorder | EDIT_MODIFIER_GROUPS | Reorder modifier groups |
| GET | /modifier-groups/{id} | VIEW_MODIFIER_GROUPS | Show modifier group |
| PUT | /modifier-groups/{id} | EDIT_MODIFIER_GROUPS | Update modifier group |
| DELETE | /modifier-groups/{id} | DELETE_MODIFIER_GROUPS | Delete modifier group |
Variant Groups
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /variant-groups | CREATE_VARIANT_GROUPS | Create variant group |
| GET | /variant-groups/{id} | VIEW_ITEMS | Show variant group |
| PUT | /variant-groups/{id} | EDIT_VARIANT_GROUPS | Update variant group |
| DELETE | /variant-groups/{id} | DELETE_VARIANT_GROUPS | Delete variant group |
Display Groups
| Method | Path | Description |
|---|---|---|
| GET | /display-groups | List all display groups |
| POST | /display-groups | Create display group |
| GET | /display-groups/{id} | Show display group |
| PUT | /display-groups/{id} | Update display group |
| DELETE | /display-groups/{id} | Delete display group |
Offers
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /offers | VIEW_OFFERS | List offers |
| POST | /offers | CREATE_OFFERS | Create offer |
| GET | /offers/{id} | VIEW_OFFERS | Show offer |
| PUT | /offers/{id} | EDIT_OFFERS | Update offer |
| DELETE | /offers/{id} | DELETE_OFFERS | Delete offer |
| POST | /offers/activate/{id} | EDIT_OFFERS | Activate offer |
| POST | /offers/deactivate/{id} | EDIT_OFFERS | Deactivate offer |
| POST | /offers/archive/{id} | EDIT_OFFERS | Archive offer |
| POST | /offers/unarchive/{id} | EDIT_OFFERS | Unarchive offer |
Transactions
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /transactions/export | EXPORT_TRANSACTION_REPORTS | Export transactions |
| GET | /transactions/stats | VIEW_TRANSACTIONS | Transaction statistics |
| GET | /transactions/datatable | VIEW_TRANSACTIONS | List transactions |
| GET | /transactions/{id} | VIEW_TRANSACTIONS | Show transaction |
| POST | /transactions/{id}/resend-receipt | VIEW_TRANSACTIONS | Resend receipt |
| POST | /transactions/{id}/retry-sync | VIEW_TRANSACTIONS | Retry a failed POS sync |
| POST | /transactions/{id}/verify-payment-status | VIEW_TRANSACTIONS | Recheck the payment at Stripe and settle if paid |
| DELETE | /transactions/{id} | VIEW_TRANSACTIONS | Delete a test-mode transaction |
Async Monitoring
The whole prefix sits behind the global-admin middleware — it exposes cross-tenant job internals.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /async-monitoring | global-admin (role) | Monitor index |
| GET | /async-monitoring/summary | global-admin (role) | Summary + recent runs |
| GET | /async-monitoring/queue?limit={n} | global-admin (role) | Live queue and failed-job tables |
VAT Verification
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /settings/vat/verify | Back-office auth only (throttle 30/min) | VIES VAT live check; fails open — always 200 with valid / invalid / unavailable / skipped |
Customers (BackOffice)
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /customers/datatable | VIEW_CUSTOMERS | List customers |
| GET | /customers/export | EXPORT_CUSTOMERS | Export customers |
| POST | /customers | CREATE_CUSTOMERS | Create customer |
| GET | /customers/order-detail/{id} | VIEW_TRANSACTIONS | Show order detail |
| GET | /customers/{id} | VIEW_CUSTOMERS | Show customer |
| PUT | /customers/{id} | EDIT_CUSTOMERS | Update customer |
| DELETE | /customers/{id} | DELETE_CUSTOMERS | Delete customer |
| GET | /customers/{id}/orders | VIEW_CUSTOMERS | Customer orders |
| GET | /customers/{id}/gift-cards | VIEW_CUSTOMERS | Customer gift cards |
| GET | /customers/{id}/reward-redemptions | VIEW_CUSTOMERS | Customer rewards |
| GET | /customers/{id}/timelines | VIEW_CUSTOMERS | Customer timeline |
| POST | /customers/{id}/notes | EDIT_CUSTOMERS | Add customer notes |
| POST | /customers/{id}/marketing | EDIT_CUSTOMERS | Update marketing preferences |
| POST | /customers/{id}/addresses | EDIT_CUSTOMERS | Add address |
| POST | /customers/{id}/addresses/{addressId} | EDIT_CUSTOMERS | Set default address |
Devices
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /devices | VIEW_DEVICES | List devices |
| POST | /devices | CREATE_DEVICES | Create device |
| POST | /devices/subscribe/{locationId} | CREATE_DEVICES | Subscribe device |
| GET | /devices/new | CREATE_DEVICES | New device form data |
| GET | /devices/options | -- | Device select options |
| GET | /devices/{id} | VIEW_DEVICES | Show device |
| PUT | /devices/{id} | EDIT_DEVICES | Update device |
| DELETE | /devices/{id} | DELETE_DEVICES | Delete 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}/profile | EDIT_DEVICES | Assign device profile |
| PATCH | /devices/{id}/reader/label | EDIT_DEVICES | Rename the paired card reader |
The following 14 /devices routes additionally carry first-party-pos (EnsureFirstPartyPos); the pre-existing kiosk/terminal routes above do not.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /devices/unconfigured-printers | VIEW_DEVICES | Printers discovered but not yet configured |
| POST | /devices/stations | EDIT_DEVICES | Create a KDS station |
| GET | /devices/stations/location/{locationId} | VIEW_DEVICES | List a location's stations |
| GET | /devices/stations/{id} | VIEW_DEVICES | Show station |
| PUT | /devices/stations/{id} | EDIT_DEVICES | Update station |
| DELETE | /devices/stations/{id} | EDIT_DEVICES | Delete station |
| POST | /devices/{id}/station | EDIT_DEVICES | Bind a KDS device to a station (null unbinds) |
| POST | /devices/{id}/printer/configure | EDIT_DEVICES | Write a printer's configuration |
| POST | /devices/{id}/printer/pairing-session | EDIT_DEVICES | Start pairing (mints/discloses the poll credential) |
| GET | /devices/{id}/printer/pairing-session | VIEW_DEVICES | Pairing status (read-only) |
| POST | /devices/{id}/printer/pairing-session/confirm-write | EDIT_DEVICES | Confirm the operator wrote the config |
| POST | /devices/{id}/printer/test-print | EDIT_DEVICES | Merchant-facing test print |
| POST | /devices/{id}/printer/rotate-token | EDIT_DEVICES | Rotate the printer's poll token |
| POST | /devices/{id}/printer/retire | DELETE_DEVICES | Retire the printer |
(Verified: routes/api/backoffice/devices.php:40-41, :65-76, :94-95, :104-105, :135-153.)
Device Profiles
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /device-profiles | VIEW_DEVICE_PROFILE | List profiles |
| POST | /device-profiles | CREATE_DEVICE_PROFILE | Create profile |
| GET | /device-profiles/options | -- | Profile options |
| GET | /device-profiles/{id} | VIEW_DEVICE_PROFILE | Show profile |
| PUT | /device-profiles/{id} | EDIT_DEVICE_PROFILE | Update profile |
| DELETE | /device-profiles/{id} | DELETE_DEVICE_PROFILE | Delete 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.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /printer-profiles | VIEW_DEVICE_PROFILE | List printer profiles |
| POST | /printer-profiles | CREATE_DEVICE_PROFILE | Create printer profile |
| GET | /printer-profiles/options | VIEW_DEVICE_PROFILE | Printer-profile options |
| GET | /printer-profiles/{id} | VIEW_DEVICE_PROFILE | Show printer profile |
| PUT | /printer-profiles/{id} | EDIT_DEVICE_PROFILE | Update printer profile |
| DELETE | /printer-profiles/{id} | DELETE_DEVICE_PROFILE | Delete printer profile |
| GET | /kds-profiles | VIEW_DEVICE_PROFILE | List KDS profiles |
| POST | /kds-profiles | CREATE_DEVICE_PROFILE | Create KDS profile |
| GET | /kds-profiles/options | VIEW_DEVICE_PROFILE | KDS-profile options |
| GET | /kds-profiles/{id} | VIEW_DEVICE_PROFILE | Show KDS profile |
| PUT | /kds-profiles/{id} | EDIT_DEVICE_PROFILE | Update KDS profile |
| DELETE | /kds-profiles/{id} | DELETE_DEVICE_PROFILE | Delete KDS profile |
| GET | /kds-reports/summary | VIEW_KITCHEN_REPORTS | Kitchen report summary |
Deliberate divergence: the legacy kiosk
/device-profiles/optionsabove is ungated, while/printer-profiles/optionsand/kds-profiles/optionsboth requireVIEW_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.
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /pos/select-provider | EDIT_IN_HOUSE_CHANNEL | Select the first-party Upvendo POS (sets the pos_provider marker) |
| GET | /pos/staff-credentials | MANAGE_POS_STAFF | List staff PIN credentials |
| GET | /pos/staff-credentials/assignable-staff | MANAGE_POS_STAFF | Users available for a credential |
| POST | /pos/staff-credentials | MANAGE_POS_STAFF | Create a staff credential |
| PUT | /pos/staff-credentials/{credentialId} | MANAGE_POS_STAFF | Update a staff credential |
| POST | /pos/staff-credentials/{credentialId}/reset-pin | MANAGE_POS_STAFF | Reset a staff PIN |
| DELETE | /pos/staff-credentials/{credentialId} | MANAGE_POS_STAFF | Delete a staff credential |
| GET | /pos/{locationId}/registers | VIEW_POS_CONFIGURATION | List registers |
| POST | /pos/{locationId}/registers | MANAGE_POS_CONFIGURATION | Create register |
| PUT | /pos/{locationId}/registers/{registerId} | MANAGE_POS_CONFIGURATION | Update register |
| POST | /pos/{locationId}/registers/{registerId}/retire | MANAGE_POS_CONFIGURATION | Retire register |
| POST | /pos/{locationId}/registers/{registerId}/pair-terminal | MANAGE_POS_CONFIGURATION | Pair a Stripe Terminal reader |
| DELETE | /pos/{locationId}/registers/{registerId}/terminal | MANAGE_POS_CONFIGURATION | Unpair the reader |
| PATCH | /pos/{locationId}/registers/{registerId}/terminal/label | MANAGE_POS_CONFIGURATION | Rename the reader |
| GET | /pos/{locationId}/drawers | VIEW_POS_CONFIGURATION | List cash drawers |
| POST | /pos/{locationId}/drawers | MANAGE_POS_CONFIGURATION | Create cash drawer |
| PUT | /pos/{locationId}/drawers/{drawerId} | MANAGE_POS_CONFIGURATION | Update cash drawer |
| POST | /pos/{locationId}/drawers/{drawerId}/retire | MANAGE_POS_CONFIGURATION | Retire cash drawer |
| GET | /pos/{locationId}/fdm-devices | VIEW_POS_CONFIGURATION | List FDM devices |
| POST | /pos/{locationId}/fdm-devices | MANAGE_POS_CONFIGURATION | Create FDM device |
| PUT | /pos/{locationId}/fdm-devices/{fdmDeviceId} | MANAGE_POS_CONFIGURATION | Update FDM device |
| GET | /pos/{locationId}/business-days | VIEW_POS_REPORTS | List business days |
| GET | /pos/{locationId}/business-days/{businessDayId} | VIEW_POS_REPORTS | Show a business day / Z-report |
| GET | /pos/{locationId}/fiscal-events | VIEW_POS_REPORTS | Fiscal 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
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /contents/datatable | -- | Content datatable |
| GET | /contents | -- | List all content |
| POST | /contents | CREATE_CONTENT | Create content |
| GET | /contents/{id} | VIEW_CONTENT | Show content |
| POST | /contents/{id} | VIEW_CONTENT | Update content |
| DELETE | /contents | DELETE_CONTENT | Delete multiple content items |
Inventories
| Method | Path | Description |
|---|---|---|
| GET | /inventories | Inventory overview |
| POST | /inventories | Create inventory history |
| DELETE | /inventories | Delete inventory entries |
Tax Rates
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /tax-rate-options | -- | Tax rate select options |
| GET | /tax-rates/datatable | VIEW_TAX_RATES | List tax rates |
| POST | /tax-rates | CREATE_CUSTOM_TAX_RATES | Create tax rate |
| PUT | /tax-rates/{id} | EDIT_CUSTOM_TAX_RATES | Update tax rate |
| DELETE | /tax-rates/{id} | DELETE_CUSTOM_TAX_RATES | Delete tax rate |
Settings Endpoints
All prefixed with /back-office/settings/ and require backoffice auth.
Locations
| Method | Path | Description |
|---|---|---|
| GET | /location-options | Location select options |
| GET | /locations/address-suggestions | Autocomplete address |
| GET | /locations/datatable | List locations |
| POST | /locations | Create location |
| GET | /locations/{id} | Show location |
| PUT | /locations/{id} | Update location |
| DELETE | /locations/{id} | Delete location |
| PUT | /locations/{id}/status | Update location status |
| POST | /locations/export | Export locations |
| GET | /locations/{id}/terminal-options | Get terminal options for location |
Team (Users & Roles)
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /team/permission-list | -- | List all permissions |
| GET | /team/roles/options | -- | Role select options |
| GET | /team/roles | VIEW_ROLES | List roles |
| GET | /team/roles/{roleId} | VIEW_ROLES | Show role |
| POST | /team/roles | CREATE_ROLES | Create role |
| PUT | /team/roles/{roleId} | EDIT_ROLES | Update role |
| DELETE | /team/roles/{roleId} | DELETE_ROLES | Delete role |
| GET | /team/users | VIEW_USERS | List users |
| POST | /team/users | CREATE_USERS | Invite user |
| POST | /team/users/{userId}/resend-invitation | CREATE_USERS | Resend invitation |
| PUT | /team/users/{userId}/roles | ASSIGN_ROLES | Assign roles |
| PUT | /team/users/{userId}/locations | EDIT_USER_LOCATION_ACCESS | Assign locations |
| PUT | /team/users/{userId} | EDIT_USERS | Update user |
| DELETE | /team/users/{userId} | DELETE_USERS | Delete user |
| GET | /team/users/global | VIEW_USERS | List global users |
| POST | /team/users/global | CREATE_USERS | Create global user |
| PUT | /team/users/global/{userId} | EDIT_USERS | Update global user |
| DELETE | /team/users/global/{userId} | DELETE_USERS | Delete global user |
Billing Profiles
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /billing-profiles | VIEW_BILLING_PROFILES | List profiles |
| GET | /billing-profiles/options | -- | Profile options |
| POST | /billing-profiles | CREATE_BILLING_PROFILES | Create profile |
| GET | /billing-profiles/{id} | VIEW_BILLING_PROFILES | Show profile |
| PUT | /billing-profiles/{id} | EDIT_BILLING_PROFILES | Update profile name |
| POST | /billing-profiles/{id} | EDIT_BILLING_PROFILES | Attach payment method |
| DELETE | /billing-profiles/{id} | DELETE_BILLING_PROFILES | Delete profile |
| GET | /billing-profiles/{id}/past-bills | VIEW_BILLING_PROFILES | Past bills datatable |
| GET | /billing-profiles/{id}/export | VIEW_BILLING_PROFILES | Export past bills |
| GET | /billing-profiles/{id}/subscriptions | VIEW_BILLING_PROFILES | List subscriptions |
| POST | /billing-profiles/{id}/subscriptions/{subId}/cancel | EDIT_BILLING_PROFILES | Cancel subscription |
| POST | /billing-profiles/{id}/subscriptions/{subId}/reactivate | EDIT_BILLING_PROFILES | Reactivate subscription |
| POST | /billing-profiles/{id}/subscriptions/{subId}/retry-payment | EDIT_BILLING_PROFILES | Retry failed payment |
Payment Profiles
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /payment-profiles | VIEW_PAYMENT_PROFILES | List profiles |
| POST | /payment-profiles | CREATE_PAYMENT_PROFILES | Create profile |
| GET | /payment-profiles/options | -- | Profile options |
| GET | /payment-profiles/{id} | VIEW_PAYMENT_PROFILES | Show profile |
| PUT | /payment-profiles/{id} | EDIT_PAYMENT_PROFILES | Update name |
| DELETE | /payment-profiles/{id} | DELETE_PAYMENT_PROFILES | Delete profile |
| POST | /payment-profiles/{id}/default | EDIT_PAYMENT_PROFILES | Set as default |
| POST | /payment-profiles/{id}/stripe/url | EDIT_PAYMENT_PROFILES | Create the Stripe onboarding link |
| PUT | /payment-profiles/{id}/stripe/descriptor | EDIT_PAYMENT_PROFILES | Update the statement descriptor |
| PUT | /payment-profiles/{id}/stripe/business-website | EDIT_PAYMENT_PROFILES | Update the Stripe business website |
| PUT | /payment-profiles/{id}/stripe/payout-schedule | EDIT_PAYMENT_PROFILES | Update the payout schedule |
| GET | /payment-profiles/{id}/stripe/payouts | VIEW_PAYMENT_PROFILES | List Stripe payouts |
| GET | /payment-profiles/{id}/stripe/payouts/datatable | VIEW_PAYMENT_PROFILES | Stripe payouts datatable |
| POST | /payment-profiles/{id}/stripe/account-session | VIEW_PAYMENT_PROFILES | Create an embedded Stripe account session |
| POST | /payment-profiles/{id}/viva-wallet/account | EDIT_PAYMENT_PROFILES | Create the Viva Wallet connected account |
(Verified: routes/api/backoffice/settings/payment-profiles.php:8-57 — 15 routes in the prefix.)
Branding Profiles
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /branding-profiles | VIEW_BRANDING_PROFILE | List profiles |
| GET | /branding-profiles/options | -- | Profile options |
| POST | /branding-profiles | CREATE_BRANDING_PROFILE | Create profile |
| POST | /branding-profiles/set-default | EDIT_BRANDING_PROFILE | Set default profile |
| GET | /branding-profiles/{id} | VIEW_BRANDING_PROFILE | Show profile |
| PUT | /branding-profiles/{id} | EDIT_BRANDING_PROFILE | Update profile |
| PUT | /branding-profiles/{id}/name | EDIT_BRANDING_PROFILE | Update name only |
| DELETE | /branding-profiles/{id} | DELETE_BRANDING_PROFILE | Delete profile |
Languages & Translations
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /languages | VIEW_LANGUAGE | List languages |
| GET | /languages/check-incomplete | VIEW_LANGUAGE | Check incomplete translations |
| POST | /languages | CREATE_LANGUAGE | Add language |
| POST | /languages/set-default/{id} | EDIT_LANGUAGE | Set default language |
| POST | /languages/publish/{id} | EDIT_LANGUAGE | Publish language |
| POST | /languages/unpublish/{id} | EDIT_LANGUAGE | Unpublish language |
| DELETE | /languages/delete/{id} | DELETE_LANGUAGE | Delete language |
| GET | /translations | VIEW_TRANSLATION | Get translations |
| POST | /translations | EDIT_TRANSLATION | Save translations |
| GET | /translations/datatable | VIEW_TRANSLATION | Translation datatable |
| POST | /translations/translate | EDIT_TRANSLATION | Auto-translate |
| GET | /translations/bulk/{id} | VIEW_TRANSLATION | Show bulk translations |
| POST | /translations/bulk/{id} | EDIT_TRANSLATION | Bulk auto-translate |
Receipts & Activity Logs
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /receipts/{locationId} | -- | Get receipt settings |
| PUT | /receipts/{locationId} | EDIT_RECEIPTS | Update receipt settings |
| GET | /activity-logs/datatable | VIEW_ACTIVITY_LOGS | List activity logs |
Online Ordering Endpoints
Public (No Auth)
| Method | Path | Description |
|---|---|---|
| GET | /online-ordering/address-suggestions | Autocomplete address search |
| GET | /online-ordering/coordinates-to-address | Reverse geocode coordinates |
| GET | /online-ordering/{slug} | Get restaurant/location info |
| POST | /online-ordering/payment/process | Process Square payment |
Restaurant Suggestions (Optional Auth)
| Method | Path | Description |
|---|---|---|
| GET | /online-ordering/restaurant-suggestions/nearby | Get nearby restaurants |
| POST | /online-ordering/restaurant-suggestions/upvote | Upvote a restaurant |
| GET | /online-ordering/restaurant-suggestions/top | Get top suggestions |
| GET | /online-ordering/restaurant-suggestions/categories | Get suggestion categories |
Order Flow (Optional Customer Auth)
| Method | Path | Middleware | Description |
|---|---|---|---|
| POST | /online-ordering/{slug}/{locationId}/offers/validate | auth.optional, tenant:online-ordering | Validate offers |
| POST | /online-ordering/{slug}/{locationId}/available-timeslots | auth.optional, tenant:online-ordering | Get available timeslots |
| POST | /online-ordering/{slug}/{locationId}/payment | auth.optional, tenant:online-ordering | Create payment |
| POST | /online-ordering/{slug}/{locationId}/verify-payment | auth.optional, tenant:online-ordering | Verify payment status |
Customer Order History
| Method | Path | Middleware | Description |
|---|---|---|---|
| POST | /customer/{slug}/order-history | auth.optional, throttle:5,1 | Get order history |
| GET | /customer/{slug}/order-detail/{orderId} | auth.optional, throttle:10,1 | Get order detail |
Table QR Ordering (Optional Auth)
| Method | Path | Description |
|---|---|---|
| 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-order | Bind order to session |
BackOffice Online Ordering Management
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /online-ordering/{locationId} | VIEW_ONLINE_ORDERING | Show settings |
| PUT | /online-ordering/{locationId} | EDIT_ONLINE_ORDERING | Update settings |
| POST | /online-ordering/{locationId}/snooze | EDIT_ONLINE_ORDERING | Snooze ordering |
| GET | /online-settings/{locationId} | -- | Show online settings |
| PUT | /online-settings/{locationId} | -- | Update online settings |
| GET | /in-house-settings/{locationId} | VIEW_SALES_CHANNEL | Show in-house settings |
| PUT | /in-house-settings/{locationId} | EDIT_IN_HOUSE_CHANNEL | Update in-house settings |
QR Ordering Management
| Method | Path | Description |
|---|---|---|
| GET | /qr-ordering/{locationId} | Show QR ordering config |
| PUT | /qr-ordering/{locationId} | Update QR ordering config |
| GET | /qr-ordering/{locationId}/qr-download-options | Get QR download options |
| POST | /qr-ordering/{locationId}/download-multiple | Download multiple QR codes |
Table Sections
| Method | Path | Description |
|---|---|---|
| POST | /table-sections | Create 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.
| Method | Path | Description |
|---|---|---|
| POST | /kiosk/payment/create-intent | Create payment intent on terminal |
| POST | /kiosk/payment/send-receipt | Send receipt to customer |
| POST | /kiosk/payment/cancel-action | Cancel terminal payment action |
| GET | /kiosk/payment/session-status | Check payment session status |
| GET | /kiosk/payment/details/{idempotencyKey} | Get payment details after completion |
| POST | /kiosk/customer/identify | Identify the customer at the kiosk |
| POST | /kiosk/loyalty | Login customer for loyalty |
| GET | /kiosk/loyalty | Get loyalty details |
| GET | /kiosk/offers | List available offers |
| POST | /kiosk/offers/validate | Validate selected offers |
| POST | /kiosk/gift-cards/validate | Validate a gift-card code |
| POST | /kiosk/pricing/quote | Price a basket before payment |
| GET | /kiosk/updates/check | Check for app updates |
| POST | /kiosk/updates/fcm/register | Register FCM token |
| POST | /kiosk/test-print-receipt | Test print a receipt |
| POST | /kiosk/printers/discovery-report | Report 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.
| Method | Path | Description |
|---|---|---|
| POST | /kds/fcm-token | Update FCM token |
| GET | /kds/orders | List current orders |
| PUT | /kds/mark-modifier | Mark modifier item status |
| PUT | /kds/mark-item | Mark order item status |
| GET | /kds/items | List KDS items |
| PUT | /kds/{id}/in-progress | Mark order in progress |
| PUT | /kds/{id}/ready | Mark order ready |
| PUT | /kds/{id}/complete | Mark order complete |
| PUT | /kds/{id}/prioritize | Prioritize order |
| PUT | /kds/{id}/hold | Hold order |
| GET | /kds/metrics/summary | Kitchen metrics summary |
| PUT | /kds/{id}/recall | Recall a completed order |
| PUT | /kds/{id}/release | Release a held order |
| PUT | /kds/{id}/unprioritize | Remove priority from an order |
| POST | /kds/customer-arrived | Customer-arrival signal (order id in the body, not the path) |
| POST | /kds/estimated-arrival | Estimated-arrival signal (order id in the body, not the path) |
| PUT | /kds/settings | Update KDS settings |
| GET | /kds/settings | Read the device's materialized settings |
| GET | /kds/categories | List 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.
| Method | Path | Extra middleware | Description |
|---|---|---|---|
| POST | /pos/staff/pin-login | throttle:pos-pin | Open a staff PIN session |
| POST | /pos/staff/pin-logout | pos.staff | Close the staff session |
| POST | /pos/staff/escalate | pos.staff, throttle:pos-escalate | Manager escalation → approval_token |
| GET | /pos/bootstrap | -- | Catalog + configuration bootstrap |
| POST | /pos/refunds | pos.staff | Signed cash refund of a settled sale |
| POST | /pos/tills | pos.staff | Open a till |
| GET | /pos/tills/current | pos.staff | Current till (bare resource; 204 when none) |
| POST | /pos/tills/{tillId}/movements | pos.staff | Typed drawer movement |
| POST | /pos/tills/{tillId}/end | pos.staff | Stop taking cash (OPEN → ENDED) |
| POST | /pos/tills/{tillId}/count | pos.staff | Blind count |
| POST | /pos/tills/{tillId}/close | pos.staff | Declare and freeze |
| GET | /pos/business-day | pos.staff | Peek the fiscal business day |
| POST | /pos/business-day/close | pos.staff | Z close |
| GET | /pos/business-day/x-report | pos.staff | Mid-day X report |
| GET | /pos/orders | pos.staff | List orders (state required: open|settling|settled) |
| PUT | /pos/orders/{clientUuid} | pos.staff | Upsert an order (optimistic version; 409 on conflict) |
| GET | /pos/orders/{clientUuid} | pos.staff | Show order |
| POST | /pos/orders/{clientUuid}/fire | pos.staff | Fire to kitchen |
| POST | /pos/orders/{clientUuid}/settle | pos.staff | Settle in cash |
| POST | /pos/orders/{clientUuid}/void | pos.staff | Void an order |
| POST | /pos/printers/discovery-report | throttle:printer-discovery | Report LAN-discovered printers (device-level, no staff session) |
| GET | /pos/printers | pos.staff | List the site's printers (optional ?state=unconfigured|paired) |
| POST | /pos/printers/{id}/configure | pos.staff | Write a printer's configuration |
| POST | /pos/printers/{id}/pairing-session | pos.staff | Start pairing |
| GET | /pos/printers/{id}/pairing-session | pos.staff | Pairing status |
| POST | /pos/printers/{id}/pairing-session/confirm-write | pos.staff | Confirm the write |
| POST | /pos/printers/{id}/test-print | pos.staff | Merchant-facing test print (no manager escalation) |
| GET | /pos/kds-stations | pos.staff | Station picker (location comes from the device token) |
| GET | /pos/channel-orders | pos.staff | Read-only omni-channel order stream |
| GET | /pos/channel-orders/{id} | pos.staff | Show 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):
| Level | Routes | Requirement |
|---|---|---|
| Device | discovery-report | Device JWT only; runs at boot before anyone signs in |
| Operator | GET /printers, GET …/pairing-session, …/confirm-write, …/test-print | Staff 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"
}
]
}printersis 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. macis the only required key (:73,App\Rules\MacAddress).ipmust passip;paper_width_mmmust be58or80;model/device_name/serialaremax:64.mac_sourceisnullable+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 asipp-uuidon the write path only (PrinterProvisioningService.php:581-584).- There is deliberately no
location_id,vendor_id,device_idortokenin 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 throughClientIpHelper(AppServiceProvider.php:291-303).
Printer Poll Endpoint
| Method | Path | Middleware | Description |
|---|---|---|---|
| POST | /print/epson/sdp | throttle:printer-poll, printer.auth | Epson 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:
| Endpoint | FormRequest | sync_time | Scope |
|---|---|---|---|
POST /square/sync-time | BackOffice/SquareIntegration/UpdateSyncTimeRequest | required | merchant |
PUT /lightspeed/sync-time | BackOffice/Lightspeed/UpdateSyncTimeRequest | nullable | merchant |
POST /mpluskassa (initiate/settings save) | BackOffice/MplusKassa/InitiateMplusKassaRequest | nullable | merchant |
POST /kassanet/{provider}/{locationId} (initiate/settings save) | BackOffice/Kassanet/InitiateKassanetRequest | nullable | location |
KassanetIntegrationController::updateAutoSyncSchedule | BackOffice/Kassanet/UpdateAutoSyncScheduleRequest | nullable | location |
The last row has no registered route —
routes/api/backoffice/kassanet.phpdeclares onlyGET,POST,POST /sync-menu,GET /testandDELETE. Hendrickx/Vanhoutte sync times are written throughPOST /kassanet/{provider}/{locationId}(initiate), which accepts the same keys.
| Key | Rules | Notes |
|---|---|---|
sync_time | string, 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_timezone | nullable, string, timezone | IANA identifier, validated by Laravel's timezone rule (PHP's timezone_identifiers_list()). Optional on all five. |
enable_auto_sync | sometimes, boolean | Square 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 null — ThirdPartyIntegration::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.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /deliveroo/{locationId} | -- | Get integration status |
| POST | /deliveroo/{locationId} | MANAGE_INTEGRATION_SETTINGS | Enable integration |
| PUT | /deliveroo/{locationId} | MANAGE_INTEGRATION_SETTINGS | Update integration |
| DELETE | /deliveroo/{locationId} | MANAGE_INTEGRATION_SETTINGS | Disable integration |
| POST | /deliveroo/{locationId}/sync-menu | MANAGE_INTEGRATION_SETTINGS | Sync menu to Deliveroo |
| POST | /deliveroo/{locationId}/sync-availability | MANAGE_INTEGRATION_SETTINGS | Push 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-status | MANAGE_INTEGRATION_SETTINGS | Set 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 onlyPUTverbs on this prefix are/settings,/store/status,/store/prep-timeand/store/fulfillment. Settings are updated throughPUT /uber-eats/{locationId}/settings.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /uber-eats/{locationId} | -- | Get integration status |
| POST | /uber-eats/{locationId} | MANAGE_INTEGRATION_SETTINGS | Enable integration |
| DELETE | /uber-eats/{locationId} | MANAGE_INTEGRATION_SETTINGS | Disable integration |
| POST | /uber-eats/{locationId}/select-store | MANAGE_INTEGRATION_SETTINGS | Bind 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}/settings | MANAGE_INTEGRATION_SETTINGS | Update 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/status | MANAGE_INTEGRATION_SETTINGS | Set store online/offline |
| PUT | /uber-eats/{locationId}/store/prep-time | MANAGE_INTEGRATION_SETTINGS | Update prep time |
| PUT | /uber-eats/{locationId}/store/fulfillment | MANAGE_INTEGRATION_SETTINGS | Update 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}/promotions | MANAGE_INTEGRATION_SETTINGS | Create a promotion |
| GET | /uber-eats/{locationId}/promotions/{promotionId} | -- | Show a promotion |
| DELETE | /uber-eats/{locationId}/promotions/{promotionId} | MANAGE_INTEGRATION_SETTINGS | Revoke a promotion |
| POST | /uber-eats/{locationId}/reports | MANAGE_INTEGRATION_SETTINGS | Request 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.
| Method | Path | Description |
|---|---|---|
| GET | /square/status | Get integration status |
| POST | /square/oauth | Initiate OAuth flow |
| POST | /square/sync-time | Update sync schedule (sync_time required, sync_timezone and enable_auto_sync optional -- see Auto-sync time payload) |
| POST | /square/disable | Disable integration |
| POST | /square/start-sync | Start manual sync |
| GET | /square/test | Test connection |
(Verified: routes/api/backoffice/square.php -- the complete file.)
Shopify
| Method | Path | Description |
|---|---|---|
| GET | /shopify/{locationId} | Get integration status |
| PUT | /shopify/{locationId} | Update integration |
| DELETE | /shopify/{locationId} | Disable integration |
| POST | /shopify/{locationId}/sync-menu | Export menu to Shopify |
| POST | /shopify/{locationId}/import-menu | Import menu from Shopify |
| POST | /shopify/{locationId}/oauth | Initiate 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.
| Method | Path | Description |
|---|---|---|
| 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-menu | Sync menu |
| GET | /kassanet/{provider}/{locationId}/test | Test 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.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /lightspeed/status | -- | Get integration status |
| POST | /lightspeed/oauth | MANAGE_INTEGRATION_SETTINGS | Initiate OAuth flow |
| POST | /lightspeed/disable | MANAGE_INTEGRATION_SETTINGS | Disable integration |
| GET | /lightspeed/test | -- | Test connection |
| GET | /lightspeed/mappings | -- | List location mappings |
| PUT | /lightspeed/mappings/{upvendoLocationId} | MANAGE_INTEGRATION_SETTINGS | Create/update a location mapping |
| DELETE | /lightspeed/mappings/{upvendoLocationId} | MANAGE_INTEGRATION_SETTINGS | Remove a location mapping |
| POST | /lightspeed/mappings/{upvendoLocationId}/pause | MANAGE_INTEGRATION_SETTINGS | Pause a mapping |
| POST | /lightspeed/mappings/{upvendoLocationId}/resume | MANAGE_INTEGRATION_SETTINGS | Resume a mapping |
| POST | /lightspeed/mappings/{upvendoLocationId}/sync | MANAGE_INTEGRATION_SETTINGS | Sync a mapping |
| PUT | /lightspeed/mappings/{upvendoLocationId}/order-settings | MANAGE_INTEGRATION_SETTINGS | Per-location account-profile (order-settings) overrides |
| PUT | /lightspeed/payment-methods | MANAGE_INTEGRATION_SETTINGS | Merchant-level payment-method to K-Series tender map |
| PUT | /lightspeed/sync-time | MANAGE_INTEGRATION_SETTINGS | Merchant-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}/adopt | MANAGE_INTEGRATION_SETTINGS | Copy the suggested menu into a merchant-owned menu |
| POST | /lightspeed/menu-suggestion/{upvendoLocationId}/dismiss | MANAGE_INTEGRATION_SETTINGS | Stop suggesting the menu |
| POST | /lightspeed/menu-suggestion/{upvendoLocationId}/use | MANAGE_INTEGRATION_SETTINGS | Idempotent 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/local | MANAGE_INTEGRATION_SETTINGS | Push a local (eat-in) order to the POS |
| POST | /lightspeed/orders/to-go | MANAGE_INTEGRATION_SETTINGS | Push 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/pay | MANAGE_INTEGRATION_SETTINGS | Apply 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.
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /mpluskassa | MANAGE_INTEGRATION_SETTINGS | Initiate integration / save settings (also carries sync_time + optional sync_timezone -- see Auto-sync time payload) |
| GET | /mpluskassa/status | -- | Get integration status |
| POST | /mpluskassa/disable | MANAGE_INTEGRATION_SETTINGS | Disable integration |
| POST | /mpluskassa/start-sync | MANAGE_INTEGRATION_SETTINGS | Start a manual sync |
| GET | /mpluskassa/test | -- | Test connection |
| GET | /mpluskassa/healthcheck | -- | Integration health check |
| GET | /mpluskassa/branches | -- | List MplusKassa branches |
| POST | /mpluskassa/locations/sync | MANAGE_INTEGRATION_SETTINGS | Sync locations |
| GET | /mpluskassa/location-mappings | -- | Get location mappings |
| PUT | /mpluskassa/location-mappings | MANAGE_INTEGRATION_SETTINGS | Update location mappings |
| GET | /mpluskassa/diagnose | -- | Diagnostics |
| POST | /mpluskassa/probe-capabilities | MANAGE_INTEGRATION_SETTINGS | Probe API capabilities |
| POST | /mpluskassa/toggle-live-inventory | MANAGE_INTEGRATION_SETTINGS | Toggle 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.
| Method | Path | Permission | Description |
|---|---|---|---|
| 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}/enable | MANAGE_INTEGRATION_SETTINGS | Enable integration for the location |
| PUT | /shopcaisse/{locationId}/token | MANAGE_INTEGRATION_SETTINGS | Rotate 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/select | MANAGE_INTEGRATION_SETTINGS | Bind the integration to a store |
| DELETE | /shopcaisse/{locationId} | MANAGE_INTEGRATION_SETTINGS | Delete the integration |
(Verified: routes/api/backoffice/shopcaisse.php -- the complete file, both prefix groups.)
Ordering Channels
| Method | Path | Description |
|---|---|---|
| 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.
| Method | Path | Middleware | Description |
|---|---|---|---|
| POST | /stripe-webhook/{countryCode} | -- | Stripe webhook handler |
| GET | /viva-webhook/{countryCode}/{eventTypeId} | -- | Viva Wallet verification handshake |
| POST | /viva-webhook/{countryCode}/{eventTypeId} | verify.viva-webhook | Viva Wallet events (IP-allowlisted) |
| POST | /shopify-webhook | verify.shopify-webhook | Shopify webhook handler |
| POST | /webhook/deliveroo/orders | verify.deliveroo-webhook | Deliveroo order events |
| POST | /webhook/deliveroo/menu | verify.deliveroo-webhook | Deliveroo menu events |
| POST | /webhook/uber-eats | verify.uber-eats-webhook | Uber Eats events |
| POST | /webhook/square | verify.square-webhook | Square events |
| POST | /webhook/shopcaisse | verify.shopcaisse-webhook | ShopCaisse events |
| POST | /webhook/mpluskassa/{event} | verify.mpluskassa-webhook | MplusKassa events (event type in the path) |
| POST | /webhook/lightspeed | verify.lightspeed-k-series-webhook | Lightspeed K-Series events |
| POST | /crm/intake | verify.crm-webhook, throttle:60,1 | Public 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
| Method | Path | Description |
|---|---|---|
| GET | /shopify-callback | Shopify OAuth callback |
| GET | /square-callback | Square OAuth callback |
| GET | /uber-eats/callback | Uber Eats OAuth callback |
| GET | /lightspeed/callback | Lightspeed K-Series OAuth callback |
| GET | /oauth/lightspeed/callback | Lightspeed 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.
| Method | Path | Description |
|---|---|---|
| POST | /ai-photo/generate | Generate an AI photo |
| GET | /ai-photo/status/{contentId} | Poll generation status |
| GET | /ai-photo/credits | Get remaining credits |
| GET | /ai-photo/credits-package-options | List credit packages |
| POST | /ai-photo/create-checkout-url | Create a Stripe checkout URL for credits |
| POST | /ai-photo/purchase-with-default | Purchase 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.
| Method | Path | Middleware | Description |
|---|---|---|---|
| GET | /guided-setup-management/video/url | auth | Get a setup video by URL (Emily integration) |
| GET | /guided-setup-management/tasks | auth, global-admin | List all tasks with video info |
| POST | /guided-setup-management/video | auth, global-admin | Create/update a video setting |
| GET | /guided-setup-management/video/{taskId} | auth, global-admin | Get a task's video setting |
| DELETE | /guided-setup-management/video/{taskId} | auth, global-admin | Delete 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
| Method | Path | Description |
|---|---|---|
| GET | /setup-status | Merchant setup completion status |
| GET | /payment-onboarding-status | Payment onboarding status |
(Verified: routes/api/backoffice/setup-status.php -- the complete file.)
Description Generator
| Method | Path | Description |
|---|---|---|
| POST | /description-generator/generate | AI-generate item description |
Loyalty (BackOffice)
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /loyalty/{locationId} | VIEW_LOYALTY | Get loyalty config |
| GET | /loyalty/{locationId}/subscription | EDIT_LOYALTY | Get subscription info |
| POST | /loyalty/{locationId} | EDIT_LOYALTY | Create/update loyalty |
| DELETE | /loyalty/{locationId} | DELETE_LOYALTY | Delete loyalty |
(Verified: routes/api/backoffice/loyalty.php -- the complete file.)
Other
| Method | Path | Description |
|---|---|---|
| POST | /client-logger | Client-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-options | Channel visibility options |
| GET | /back-office/item-pricing-options | Item pricing type options |
| GET | /back-office/language-options | Language select options |
| GET | /back-office/features | Feature 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.)