Skip to content

Upvendo POS Onboarding Flow

Upvendo POS is staged — test merchants only. The upvendo provider is 'active' => true, 'test_only' => true (upvendo-backend/config/pos-providers.php:51-52). The test_only check runs before the first-party bypass in both branches of the provider list (app/Services/BackOffice/ResellerService.php:675 and :731), so in production GET /api/back-office/merchant/reseller-providers returns the Upvendo POS tile only for merchants flagged is_test (test environments see it for everyone). Selection is enforced server-side too: FirstPartyPosProvisioningService::selectFirstPartyPos() rejects a live (non-test) merchant in production with a 403 "Unsupported POS provider." while test_only is set (app/Services/BackOffice/Pos/FirstPartyPosProvisioningService.php:70-77). Going GA means dropping test_only from the config entry; active stays the hard kill-switch (false hides the provider from everyone, test merchants included).

The register server is real and complete (routes/api/pos.php, app/Http/Controllers/Api/Pos/, routes/api/backoffice/pos.php). Everything below describes the flow for a merchant who can see the tile, and for a merchant who already carries the pos_provider = 'upvendo' marker. That marker has exactly one writer, FirstPartyPosProvisioningService::selectFirstPartyPos() (app/Services/BackOffice/Pos/FirstPartyPosProvisioningService.php:59), reachable from POST /api/back-office/pos/select-provider and from the internal seeders (php artisan pos:seed-demoapp/Services/Pos/PosDemoSeederService.php:254, app/Console/Commands/PosSeedDemoCommand.php:32).

Overview

Upvendo POS is Upvendo's own first-party point-of-sale — a staff-operated register (iPad, landscape-first) that is part of the Upvendo platform itself, not a third-party integration. Because it is first-party, there is no external POS to connect and no "Connect …" step: selecting Upvendo POS marks the merchant as first-party POS-connected directly (POST /api/back-office/pos/select-provider with { provider: "upvendo" }), and onboarding moves straight into setting up the register. That selection is staged — see the gate note at the top of this document; in production only merchants flagged is_test can see and choose Upvendo POS while test_only is set.

No country filter. first_party: true makes ResellerService::getMerchantResellerProviders() skip the country and reseller-catalog filters (app/Services/BackOffice/ResellerService.php:682 and :736), and the countries list in config/pos-providers.php:45-47 is display-only. The test_only check runs before that bypass (:675, :731), so in production the tile is offered — in every country — only to test merchants until test_only is dropped.

The menu lives in Upvendo. With a third-party POS the catalog is pulled from the POS; with Upvendo POS Upvendo is the source of truth — you build your menu in Upvendo (Menu Builder) and make items visible on the POS channel, alongside Kiosk and Online Ordering. There is no menu sync and no "menu must already exist in the POS" abort. Two ordering details matter:

  • POS only becomes a selectable visibility/pricing channel once the location actually has a POS footprint — at least one non-retired register or one POS device (app/Services/Orchestrators/DynamicConstantOrchestrator.php:54-56, :97). Set up the register first, or the POS channel simply will not be in the item form.
  • Adding your first POS device runs a one-time, additive per-location backfill that adds the POS channel to every item and modifier group already visible on Kiosk or Online Ordering (app/Services/Pos/PosMenuBackfillService.php:11-27). It never re-runs, so a later "hide from POS" choice survives adding another device or register.

Payments — cash first. Cash is native to the register: till open, drawer movements, count, close and the fiscal business day / X- and Z-reports all live in the POS API (routes/api/pos.php:38-53). Settling an order currently accepts cash only — the settle request takes a single amount_tendered (cents) and is documented as "Cash-only in the walking skeleton" (app/Http/Requests/Pos/SettleOrderRequest.php:19-24). Card at the counter is Stripe Terminal and is pairing only so far: a reader is paired to a register from the back office (POST /api/back-office/pos/{locationId}/registers/{registerId}/pair-terminal), which requires the location's Payment Profile to use Stripe as its in-person provider and to have a synced Stripe Terminal Location — otherwise it fails with "Terminal pairing is only available for Stripe Terminal." or "Stripe Terminal Location is still syncing. Try again in a moment." (app/Services/BackOffice/Pos/PosConfigurationService.php:200-214). So a Payment Profile is not required to build and preview your menu, kiosk and online ordering — but it is required before you can pair a card reader or take live payments, and kiosk and online-ordering payments use the same profile.

No in-house exclusivity check on this path. Selecting Upvendo POS creates no ThirdPartyIntegration, and upvendo is not in either in-house channel list, so the "Another in-house channel is already enabled for this merchant" guard never sees it (app/Helpers/ThirdPartyIntegrationHelper.php:16-18, :137-139). selectFirstPartyPos() validates only the provider and, when a location_id is supplied, that you own it — it does not look for an existing POS connection (app/Services/BackOffice/Pos/FirstPartyPosProvisioningService.php:58-99). Nothing blocks the combination, and nothing supports it either: treat Upvendo POS as one POS per merchant.


Integration Type

  • Status: Staged (test_only)'active' => true, 'test_only' => true (config/pos-providers.php:51-52): offered only to test merchants in production. See the note at the top of this document
  • Scope: First-party (native Upvendo POS — no third-party integration is created)
  • Availability: Not country-filtered — first_party: true bypasses the country and reseller-catalog filters (ResellerService.php:682, :736); the countries list at config/pos-providers.php:45-47 is display-only. The test_only check runs before the bypass (:675, :731), so live merchants are not offered it in production until test_only is dropped
  • Selection: POST /api/back-office/pos/select-provider { provider: "upvendo" }no OAuth and no credentials step. On success it returns { pos_provider: "upvendo", pos_connected: true } and mirrors the marker onto every location you own (FirstPartyPosProvisioningService.php:88-98). Gated by the edit-in-house-channel permission (routes/api/backoffice/pos.php:39-40)
  • Menu: Built in Upvendo (Menu Builder); items become available to the register by carrying the POS visibility channel — Upvendo is the source of truth (no pull/sync)
  • Payment Profile: Not blocking for menu/kiosk/online setup — required (with Stripe as the in-person provider) before pairing a Stripe Terminal reader or taking live payments
  • Branding Profile: Recommended for kiosk/storefront rendering (not blocking)
  • Billing Profile: Optional (Upvendo platform invoicing only). POS devices themselves are free — they open no device subscription and are exempt from the active-subscription check (app/Services/BackOffice/DeviceService.php:769-775, app/Services/AuthDeviceService.php:156-158)
  • Register setup: Add a POS device in Device Management → Devices (/device-management/devices), create registers in POS → Registers (/pos/registers), and give each staff member POS access from Settings → Team → Users (/settings/team/users) — open the person and use the POS access panel — then activate the device in the Upvendo POS app (iPad, landscape-only)

Discovery Flow (JSON - for deterministic parsing)

The discovery questions and the dynamic step list are produced by Emily (the proxy/back-end onboarding service), not hard-coded in the back office. After a merchant selects Upvendo POS, the back office calls select-provider and then saves { pos_provider: "upvendo", phase: "discovery" } to Emily's onboarding state and opens the chat (upvendo-backoffice/src/components/emily/EmilyWidget.vue:1616-1624); the proxy looks the doc up by type eq 'onboarding' and pos_provider eq 'upvendo' (upvendo-backend-proxy/src/emily/shared/setup-steps.js:40) and returns the checklist below. No step here connects a third-party POS. The back office falls back to BASE_SETUP_STEPS.upvendo (upvendo-backoffice/src/components/dialogs/guided-setup/types.ts:127-133) only if these steps are unavailable.

discovery_questions
[
  {
    "id": "channels",
    "text": "Which ordering channels do you want to set up with Upvendo POS?",
    "text_i18n": {
      "nl": "Welke bestelkanalen wil je instellen met Upvendo POS?",
      "fr": "Quels canaux de commande souhaitez-vous configurer avec Upvendo POS ?",
      "de": "Welche Bestellkanäle möchten Sie mit Upvendo POS einrichten?",
      "es": "¿Qué canales de pedido quieres configurar con Upvendo POS?",
      "it": "Quali canali di ordinazione vuoi configurare con Upvendo POS?",
      "pt": "Que canais de pedido quer configurar com o Upvendo POS?"
    },
    "options": [
      "Kiosk only",
      "Online ordering only",
      "Both"
    ],
    "labels": {
      "nl": {
        "Kiosk only": "Alleen kiosk",
        "Online ordering only": "Alleen online bestellen",
        "Both": "Beide"
      },
      "fr": {
        "Kiosk only": "Kiosque uniquement",
        "Online ordering only": "Commande en ligne uniquement",
        "Both": "Les deux"
      },
      "de": {
        "Kiosk only": "Nur Kiosk",
        "Online ordering only": "Nur Online-Bestellung",
        "Both": "Beides"
      },
      "es": {
        "Kiosk only": "Solo quiosco",
        "Online ordering only": "Solo pedidos en línea",
        "Both": "Ambos"
      },
      "it": {
        "Kiosk only": "Solo chiosco",
        "Online ordering only": "Solo ordini online",
        "Both": "Entrambi"
      },
      "pt": {
        "Kiosk only": "Apenas quiosque",
        "Online ordering only": "Apenas pedidos online",
        "Both": "Ambos"
      }
    }
  }
]

One question only — there is deliberately no "do you already have a menu?" question. With a third-party POS that question is meaningful: the catalog lives in the POS, so a merchant can arrive with it already built. Upvendo POS is first-party and Upvendo is the source of truth, so the menu is always built here and Build & Publish Your Menu is required either way. The answer would change nothing — no step in this document carries a skip_if, which is the only mechanism a menu_ready answer drives (upvendo-backend-proxy/src/emily/merchant/onboarding-state.js:421-423).

setup_steps
{
  "Kiosk only": [
    {"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended for kiosk/storefront rendering (colors, fonts, logo). Not blocking."},
    {"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
    {"id": "menu_items", "title": "Build & Publish Your Menu", "route": "/menus/menu-builder", "note": "Build your menu in Upvendo (the source of truth) and give items the POS and Kiosk visibility channels. POS only appears as a channel once the location has a register or a POS device."},
    {"id": "connect_pos", "title": "Set up your Upvendo POS", "route": "/device-management/devices", "note": "First-party — no third-party POS is connected. Add a POS device in Device Management -> Devices and activate it in the Upvendo POS app with the code shown here, create your registers in POS -> Registers (/pos/registers), and give each team member a register passcode from Settings -> Team -> Users (/settings/team/users) using the POS access panel on the person."},
    {"id": "in_house_settings", "title": "Set up In-House Settings", "route": "/in-house/settings"},
    {"id": "device_profile", "title": "Create Device Profile", "route": "/device-management/profiles", "note": "For the self-service kiosk device."},
    {"id": "kiosk_device", "title": "Set up Kiosk & Activate Payment", "route": "/device-management/devices"},
    {"id": "payment_profile", "title": "Set up Payment Profile", "route": "/settings/payments", "optional_for_go_live": true, "note": "Not required to build and preview your menu and kiosk — set this up before you go live / take live payments. The register settles in cash on its own; pairing a Stripe Terminal card reader to a register requires this profile to use Stripe as its in-person provider. Kiosk payments use the same profile."},
    {"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only."},
    {"id": "test_kiosk", "title": "Activate Kiosk & Test Order", "optional_for_go_live": true}
  ],
  "Online ordering only": [
    {"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended for storefront rendering (colors, fonts, logo). Not blocking."},
    {"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
    {"id": "menu_items", "title": "Build & Publish Your Menu", "route": "/menus/menu-builder", "note": "Build your menu in Upvendo (the source of truth) and give items the POS and Online Ordering visibility channels. POS only appears as a channel once the location has a register or a POS device."},
    {"id": "connect_pos", "title": "Set up your Upvendo POS", "route": "/device-management/devices", "note": "First-party — no third-party POS is connected. Add a POS device in Device Management -> Devices and activate it in the Upvendo POS app with the code shown here, create your registers in POS -> Registers (/pos/registers), and give each team member a register passcode from Settings -> Team -> Users (/settings/team/users) using the POS access panel on the person."},
    {"id": "online_settings", "title": "Set up Online Settings", "route": "/online-settings"},
    {"id": "configure_online_ordering", "title": "Set up Online Ordering", "route": "/online/online-ordering"},
    {"id": "payment_profile", "title": "Set up Payment Profile", "route": "/settings/payments", "optional_for_go_live": true, "note": "Not required to build and preview your menu and online ordering — set this up before you go live / take live payments. The register settles in cash on its own; pairing a Stripe Terminal card reader to a register requires this profile to use Stripe as its in-person provider. Online-ordering payments use the same profile."},
    {"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only."},
    {"id": "test_online_ordering", "title": "Open Online Ordering URL & Test Order", "route": "/online/online-ordering", "optional_for_go_live": true}
  ],
  "Both": [
    {"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended for kiosk/storefront rendering (colors, fonts, logo). Not blocking."},
    {"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
    {"id": "menu_items", "title": "Build & Publish Your Menu", "route": "/menus/menu-builder", "note": "Build your menu in Upvendo (the source of truth) and give items the POS, Kiosk and Online Ordering visibility channels. POS only appears as a channel once the location has a register or a POS device."},
    {"id": "connect_pos", "title": "Set up your Upvendo POS", "route": "/device-management/devices", "note": "First-party — no third-party POS is connected. Add a POS device in Device Management -> Devices and activate it in the Upvendo POS app with the code shown here, create your registers in POS -> Registers (/pos/registers), and give each team member a register passcode from Settings -> Team -> Users (/settings/team/users) using the POS access panel on the person."},
    {"id": "in_house_settings", "title": "Set up In-House Settings", "route": "/in-house/settings"},
    {"id": "device_profile", "title": "Create Device Profile", "route": "/device-management/profiles", "note": "For the self-service kiosk device."},
    {"id": "kiosk_device", "title": "Set up Kiosk & Activate Payment", "route": "/device-management/devices"},
    {"id": "online_settings", "title": "Set up Online Settings", "route": "/online-settings"},
    {"id": "configure_online_ordering", "title": "Set up Online Ordering", "route": "/online/online-ordering"},
    {"id": "payment_profile", "title": "Set up Payment Profile", "route": "/settings/payments", "optional_for_go_live": true, "note": "Not required to build and preview your menu, kiosk and online ordering — set this up before you go live / take live payments. The register settles in cash on its own; pairing a Stripe Terminal card reader to a register requires this profile to use Stripe as its in-person provider. Kiosk and online-ordering payments use the same profile."},
    {"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only."},
    {"id": "test_kiosk", "title": "Activate Kiosk & Test Order", "optional_for_go_live": true},
    {"id": "test_online_ordering", "title": "Open Online Ordering URL & Test Order", "route": "/online/online-ordering", "optional_for_go_live": true}
  ]
}

There is no connect_pos step for Upvendo POS — it is first-party, so selecting it already marks the merchant POS-connected. The core register setup is the Set up your Upvendo POS step (add a device, create registers, set up staff PINs) plus Build & Publish Your Menu. connect_pos (the retired pos_setup spelling normalizes to it via canonicalizeStepId) is auto-completed from setup-status as soon as pos_connected is true with pos_provider = 'upvendo' — there is no integration test to pass, because there is no external integration (upvendo-backoffice/src/utils/emily/setupStepMappings.ts:67-70).


Prerequisites

The one hard prerequisite is the platform-side gate described at the top: the upvendo provider must be active. Beyond that there is no connect-time prerequisite check, because there is no third-party integration to connect — selecting Upvendo POS enables the first-party register directly. Onboarding is menu-first: build your location, register and menu before payment/billing. The following are genuine setup requirements, not connect gates:

TaskRouteRequired?
Location/settings/locationsRequired — the register operates per location
POS device / register/device-management/devices, /pos/registersRequired — and needed before POS shows up as a menu visibility channel
Menu/menus/menu-builderRequired — built in Upvendo, with items carrying the POS channel
POS access (passcodes)/settings/team/users → open a user → POS accessRequired — no one can sign in at the register without a passcode
Branding Profile/settings/brandRecommended for kiosk/storefront rendering (not blocking)
Payment Profile/settings/paymentsNot blocking setup. Required — with Stripe as the in-person provider — before pairing a card reader or taking live payments; the register settles cash without it
Billing Profile/settings/billingOptional (Upvendo platform invoicing only). POS devices are free

Permissions. Every POS back-office page is gated twice: on a POS permission (view-pos-configuration, manage-pos-configuration, manage-pos-staff, manage-pos-layouts, view-pos-reports) and on the merchant actually being first-party — the first-party-pos middleware server-side (routes/api/backoffice/pos.php:43, :60) and the firstPartyPosOnly route flag in the back office (upvendo-backoffice/src/@layouts/plugins/casl.ts:103 for nav items and :168 for the route guard; since #2427 the guard uses to.matched.every(...), so a route-level gate is genuinely enforced on direct navigation — the previous .some() made every route gate vacuous). The POS and Kitchen Display tiles in the add-device dialog are shown but disabled, not hidden: every device type renders in the same v-for, and a non-first-party merchant gets a card-disabled class plus an explanatory tooltip (upvendo-backoffice/src/constants.ts:57, src/views/devices/components/dialogs/SelectDeviceTypeDialog.vue:44-50). The server rejects creating either type for a non-first-party merchant with "This feature is only available to merchants using the first-party Upvendo POS." (app/Services/BackOffice/DeviceService.php:749-761). Without the pos_provider = 'upvendo' marker, none of it is reachable.


Set up your Upvendo POS (register)

Upvendo POS runs on a staff iPad — the native shell is iPad-only and locked to landscape (upvendo-pos/capacitor.config.ts:4-8, upvendo-pos/ios/App/App/Info.plist:44-53). Once the pos_provider = 'upvendo' marker is set:

  1. Add a POS deviceDevice Management → Devices (/device-management/devices) → add a device of type POS. The tile is disabled for anyone who is not first-party, and the server enforces the same rule (upvendo-backoffice/src/constants.ts:57, app/Services/BackOffice/DeviceService.php:749-761). The new device shows an activation code; POS devices are free and open no subscription.

  2. Activate the device — open the Upvendo POS app on the iPad and enter the activation code (upvendo-pos/src/pages/DeviceActivation.vue). A code that matches no device returns "Invalid activation code" (app/Services/AuthDeviceService.php:170-177).

  3. Create your registersPOS → Registers (/pos/registers). A register needs a terminal_code (its GKS terminal id — create-only, stable for life) and a name; you can optionally bind an FDM device and one or more POS devices to it (app/Http/Requests/BackOffice/Pos/StorePosRegisterRequest.php:28-35). Registers are retired, never deleted (POST …/registers/{registerId}/retire).

  4. Give your staff POS accessSettings → Team → Users (/settings/team/users). Open the team member and use the POS access panel on their profile: give them an exactly 4-digit passcode (the register auto-submits on the fourth digit), optionally an INSZ (11 digits, write-only — encrypted and never returned) and a wallet flag (app/Http/Requests/BackOffice/Pos/StorePosStaffCredentialRequest.php:22-36). What they may do at the register comes from the roles already assigned to that user — assign "Cashier" or "Shift manager" and the passcode is the only extra setup. Credentials are merchant-wide, not per location, and there is one per team user.

    The standalone POS → Staff PINs roster (/pos/staff) was retired on 2026-07-30. The route still resolves — it redirects to /settings/team/users (upvendo-backoffice src/plugins/1.router/additional-routes.ts:70-74) — and the nav row is gone. A merchant's POS people are their team users.

  5. Put the menu on the POS channel — in Menu Builder (/menus/menu-builder), make sure your items carry the POS visibility channel. Adding the first POS device backfills that channel onto everything already visible on Kiosk or Online Ordering, so most menus need no manual work. Setup status counts the menu as ready once at least one item is visible on POS (app/Services/SetupStatusService.php:395-406).

Once the device is activated, staff sign in with their PIN, open a till, take orders, fire to the kitchen and settle — in cash; card at the counter needs a paired Stripe Terminal reader, and the settle endpoint itself is cash-only today.


Troubleshooting

Upvendo POS is not in my list of POS providers

Expected for a live (non-test) merchant in production: the provider is staged as test_only (config/pos-providers.php:52), and the test_only filter runs before the first-party bypass (ResellerService.php:675, :731). The tile appears for merchants flagged is_test and in test environments; for everyone else it stays hidden until Upvendo drops test_only.

"Unsupported POS provider." from POST /api/back-office/pos/select-provider

Same gate, seen from the API. A live merchant in production gets a 403 from the test_only check in FirstPartyPosProvisioningService::selectFirstPartyPos() (FirstPartyPosProvisioningService.php:70-77). A provider that is not first_party + active fails request validation with a 422 instead (app/Http/Requests/BackOffice/Pos/SelectPosProviderRequest.php:28-37). Neither is a permission or payload problem.

"This feature is only available to merchants using the first-party Upvendo POS."

You tried to create a POS or Kitchen Display device without the pos_provider = 'upvendo' marker; the server returns 403 (app/Services/BackOffice/DeviceService.php:749-761). The POS menu section (/pos/*) is hidden for the same reason.

"POS" is missing from the item visibility channels

POS is only offered once the location has a POS footprint — at least one non-retired register or one POS device (app/Services/Orchestrators/DynamicConstantOrchestrator.php:54-56, :97). Create the register or add the device first, then reopen the item.

The register shows no menu

Check that your items carry the POS visibility channel in Menu Builder (/menus/menu-builder). Upvendo is the source of truth — there is no external menu to sync, so an empty register means nothing is marked visible on POS.

Staff can't sign in at the register

Each staff member needs their own 4-digit passcode, set in Settings → Team → Users (/settings/team/users) — open the person and use the POS access panel. Passcodes are never shown again after they are set — use reset passcode on the person rather than trying to read the old one.

If they can sign in but a specific action is refused, that is capabilities, not the passcode: what a person may do at the register comes from the roles assigned to that team user (see Team Management). Assign "Cashier" or "Shift manager", or add a per-person exception under Custom exceptions for this person in the same panel.

The device won't activate

Activation codes do not expire on a timer — the app looks the code up as-is, so a failure means it does not match any device (app/Services/AuthDeviceService.php:170-177). Issue a fresh code from the device row in Device Management → Devices; doing so replaces the previous code (app/Services/BackOffice/DeviceService.php:938-950). Resends are rate-limited: after three within the cooldown window you get "You can resend activation code after N minutes" (default 15 minutes, app/Services/BackOffice/DeviceService.php:927-931, config/upvendo.php:38).

Pairing a card reader fails

Terminal pairing needs the location's Payment Profile to use Stripe as its in-person provider — otherwise "Terminal pairing is only available for Stripe Terminal." If the profile is right but the Stripe Terminal Location has not synced yet you get "Stripe Terminal Location is still syncing. Try again in a moment." (app/Services/BackOffice/Pos/PosConfigurationService.php:200-214).