Appearance
Upvendo POS Onboarding (Reseller)
Overview
Upvendo POS is staged — only test-flagged merchants can activate it in production.
config/pos-providers.php:51-52sets'active' => true, 'test_only' => trueon theupvendoprovider, and thetest_onlycheck runs before thefirst_partybypass in both branches ofResellerService::getMerchantResellerProviders()(ResellerService.php:675and:731): a client merchant sees the Upvendo POS tile only if the merchant record is flaggedis_test(test environments show it to everyone). The server side is real and complete — the selection endpoint, the provisioning service, registers, staff passcodes. General availability comes when Upvendo dropstest_onlyfrom the config entry. Do not commit an Upvendo POS go-live date to a live client merchant off this document.
Can a reseller route around the test_only gate?
No. There are three independent gates, and neither a partner admin nor the global admin who configures the reseller can pass any of them for a live merchant:
- You cannot put Upvendo POS in a reseller's catalog. A reseller's provider list is the
pos_providersarray on the Reseller record (Reseller.php:44, read viagetPosProviders()at:137), set by a global admin from a picker fed byGET /back-office/pos-providers(ResellerController::getPosProviders, routeresellers.php:9). That endpoint strips every first-party provider before returning (ResellerController.php:608) — first-party is not a reseller-sold product — soupvendonever appears in the picker and cannot be assigned from the back office. Note this is a picker guarantee, not a validation one:StoreResellerRequestandUpdateResellerRequestvalidatepos_providers.*as a barestringwith no allow-list, so a hand-crafted API call can still writeupvendoonto a reseller record. Gate 2 makes such a write inert for provider listing, but it is not inert everywhere — see shared/guides/permissions.md for why it still matters to commission-rule resolution. - Even if
upvendowere inpos_providers, listing is decided by the merchant's flag. In the reseller branch ofgetMerchantResellerProviders()theactivecheck runs first (ResellerService.php:726), then thetest_onlycheck against the client merchant'sis_testflag (:731), before both thefirst_partybypass (:736) and the reseller-key filter. A staged provider is dropped for a live merchant regardless of what the reseller is assigned. The merchant-with-no-reseller branch has the same ordering (:668,:675,:682). - A direct API call for a live merchant is rejected.
POST /api/back-office/pos/select-provider { provider: "upvendo" }passesSelectPosProviderRequestvalidation (the provider isfirst_partyandactive), butFirstPartyPosProvisioningService::selectFirstPartyPos()throws a 403 "Unsupported POS provider." when the target merchant is notis_testin production whiletest_onlyis set (FirstPartyPosProvisioningService.php:70-77). This holds in client mode too: acting for a client merchant only adds?vendor_id=for theadmin-vendor-overridemiddleware (pos.php:40), which rebinds which merchant is targeted — the gate then evaluates that merchant'sis_testflag.
Only a backend config change (dropping test_only in config/pos-providers.php) opens this for live merchants. There is no reseller setting, permission, role, or country that does.
How it will work (once enabled)
How a reseller (partner) onboards a client merchant who will run Upvendo POS, Upvendo's own first-party point-of-sale. There are two phases:
- Reseller-side (partner context): the reseller creates a client merchant from the Clients page and sends login credentials. This phase works today — it is not gated by the POS provider flag.
- Merchant-side (client context): the reseller (in "client mode") or the merchant selects Upvendo POS and sets up the register. This phase is open only for test-flagged client merchants (see the gate above). Selecting it marks the merchant first-party POS-connected directly (
POST /api/back-office/pos/select-provider { provider: "upvendo" }) — because Upvendo POS is first-party, there is no third-party connect step.
Key facts:
- Upvendo POS is a first-party product, not a reseller-sold one. It is deliberately hidden from the reseller-assignment picker (
ResellerController.php:608), so it is never part of a reseller's catalog.getMerchantResellerProviders()returns it withfirst_party: trueto client merchants of every reseller in every country — thefirst_partyflag bypasses the reseller-key and country filters (ResellerService.php:736) — subject to thetest_onlygate: in production only client merchants flaggedis_testreceive it untiltest_onlyis dropped. It is not scoped bypos_providersthe way third-party providers are. - Client creation does NOT pick a POS provider. POS selection happens later, inside the client's merchant context.
- No credentials/OAuth step — Upvendo POS is native to Upvendo, so there is nothing to connect.
FirstPartyPosProvisioningService::selectFirstPartyPos()writespos_provider = 'upvendo'on the merchant and mirrors it onto every location the merchant owns; it creates noThirdPartyIntegrationrecord. - The menu is built in Upvendo (Menu Builder) and published to the register/kiosk/online — Upvendo is the source of truth (no pull/sync).
- Payment Profile is deferred (not blocking) — needed before the merchant goes live on kiosk or online ordering, which the Payment Profile powers.
- At the POS counter itself, settlement is cash-only today.
routes/api/pos.phpis labelled the walking skeleton;SettleOrderRequestaccepts onlyamount_tenderedin cents (SettleOrderRequest.php:20-22), and POS refunds are "cash-only v1" (pos.php:34). A Stripe Terminal (WisePOS) reader can be paired to a register from the back office (pos.php:74,PairPosTerminalRequest), but there is no card tender in the settle path on production. Do not promise a client merchant card-at-the-counter. - No branding-profile gate at connect. The
ThirdPartyIntegrationHelper::checkRequiredProfilesbranding requirement applies only to Hendrickx, Vanhoutte, Shopcaisse and MplusKassa (ThirdPartyIntegrationHelper.php:154-159); the first-party path never calls it. Branding is still needed for kiosk/storefront rendering.
See the merchant flow for the full register setup: Upvendo POS Onboarding. Shared references: Guided Setup, Payment Profiles.
Reseller-Specific Surface
Creating the client merchant
A Partner Admin (reseller-owner, Roles.php:32, or reseller-administrator, Roles.php:34) creates a client merchant. This works today, independently of the Upvendo POS gate.
- Go to Clients (
/clients—src/pages/clients/index.vue) - Open the Add New Client dialog (
ClientFormDialog.vue) - Fill in the client's details. The dialog renders exactly eight fields (
ClientFormDialog.vue:38-47, template:133-190): First Name*, Last Name*, Email*, Temporary Password*, Phone, Business Name, Locale* (defaults toBelgium) and Default Language* (defaults toEnglish). There is no POS-provider field here, and no billing section — thebilling_section/billing_company_name/billing_vatkeys in the locale files are dead, used by no component. - Save — this POSTs to
POST /back-office/partner/merchants(ResellerController::createMerchant, routeresellers.php:54). The caller's ownreseller_idis used; a partner with no reseller gets a 400 ("Partner is not associated with a reseller").
Sending credentials
From the client detail page (/clients/:id), Send Credentials issues a temporary password to the merchant owner (POST /back-office/partner/merchants/{merchantId}/send-credentials, route resellers.php:55). A reseller can only send credentials for a merchant whose reseller_id matches their own — a mismatch 403s ("You do not have access to this merchant", ResellerController.php:580-582). This works today.
Selecting Upvendo POS (client mode) — test-flagged clients only
When the partner operates in a specific client's context, Emily runs in client mode (the Emily endpoint resolves to emily-merchant, EmilyWidget.vue:234). This is the standard merchant onboarding flow, scoped to the client. The POS-selection screen shows only what getMerchantResellerProviders returns for that client — which in production includes Upvendo POS only when the client merchant is flagged is_test (see the gate at the top).
What a reseller sees for a live client: Upvendo POS is simply absent from the client's POS-selection screen. There is no error, no "coming soon" card, no disabled tile — the provider list just does not contain it. If a partner tries to force it (a hand-made POST /api/back-office/pos/select-provider { provider: "upvendo" }, with or without ?vendor_id=), the response is a 403 with "Unsupported POS provider." (FirstPartyPosProvisioningService.php:70-77).
What will happen once it is enabled: selecting Upvendo POS calls POST /api/back-office/pos/select-provider { provider: "upvendo" }, which marks the client merchant first-party POS-connected — no OAuth, no credentials, no "Connect …" step, and no ThirdPartyIntegration row. In client mode the backoffice adds ?vendor_id=<client merchant id> so the admin-vendor-override middleware (pos.php:40) targets the client rather than the partner. Onboarding then continues into the first-party register setup below.
Discovery Flow (JSON - for deterministic parsing)
These steps are not reachable today. Only
_reseller_prerequisites(create the client, send credentials) runs on production; everything after it starts with selecting Upvendo POS, which theactivegate blocks. Treat the rest as the planned sequence, not as instructions to give a client merchant now.
discovery_questions
[
{
"id": "channels",
"text": "Which ordering channels for this client merchant?",
"text_i18n": {
"nl": "Welke bestelkanalen voor deze klant?",
"fr": "Quels canaux de commande pour ce marchand client ?",
"de": "Welche Bestellkanäle für diesen Kunden?",
"es": "¿Qué canales de pedido para este comercio cliente?",
"it": "Quali canali di ordinazione per questo commerciante cliente?",
"pt": "Que canais de pedido para este comerciante cliente?"
},
"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 "does the merchant already have a menu?" question. That question only makes sense for a third-party POS, where the catalog lives in the POS and can already exist. 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 amenu_readyanswer drives (upvendo-backend-proxy/src/emily/merchant/onboarding-state.js:421-423).
setup_steps
{
"_reseller_prerequisites": [
{"id": "create_client", "title": "Create the Client Merchant", "route": "/clients", "modal_type": "ClientFormDialog", "note": "Partner Admin only. POSTs to /back-office/partner/merchants. No POS provider is chosen here; Upvendo POS is selected later in the client's context."},
{"id": "send_credentials", "title": "Send Login Credentials", "route": "/clients/:id", "note": "Sends a temp password to the merchant owner. Then operate in the client's context (client mode) to continue."}
],
"Kiosk only": [
{"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended for kiosk/storefront rendering. Not blocking."},
{"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
{"id": "menu_items", "title": "Build & Publish Your Menu", "route": "/menus/menu-builder", "note": "Built in Upvendo (source of truth) and published to the Upvendo POS register and kiosk."},
{"id": "connect_pos", "title": "Set up Upvendo POS", "route": "/device-management/devices", "note": "First-party — no third-party POS is connected. Add and activate the POS device here. Registers live in a SEPARATE 'POS' nav group — /pos/registers — which stays hidden until the merchant actually carries the first-party marker (firstPartyPosOnly gate). Register passcodes are NOT there: they live on the person, in Settings -> Team -> Users -> 'POS access' (/settings/team/users). The old /pos/staff roster was retired on 2026-07-30 and now redirects there."},
{"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 the menu and kiosk — set this up before the merchant goes live / takes live payments. It powers kiosk payments. At the Upvendo POS counter itself, settlement is cash-only on production."},
{"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only. Reseller billing/commissions are handled separately."}
],
"Online ordering only": [
{"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended for storefront rendering. Not blocking."},
{"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
{"id": "menu_items", "title": "Build & Publish Your Menu", "route": "/menus/menu-builder", "note": "Built in Upvendo (source of truth) and published to the Upvendo POS register and online ordering."},
{"id": "connect_pos", "title": "Set up Upvendo POS", "route": "/device-management/devices", "note": "First-party — no third-party POS is connected. Add and activate the POS device here. Registers live in a SEPARATE 'POS' nav group — /pos/registers — which stays hidden until the merchant actually carries the first-party marker (firstPartyPosOnly gate). Register passcodes are NOT there: they live on the person, in Settings -> Team -> Users -> 'POS access' (/settings/team/users). The old /pos/staff roster was retired on 2026-07-30 and now redirects there."},
{"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 the menu and online ordering — set this up before the merchant goes live / takes live payments. It powers online-ordering payments. At the Upvendo POS counter itself, settlement is cash-only on production."},
{"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only."}
],
"Both": [
{"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended for kiosk/storefront rendering. Not blocking."},
{"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
{"id": "menu_items", "title": "Build & Publish Your Menu", "route": "/menus/menu-builder", "note": "Built in Upvendo (source of truth) and published to the Upvendo POS register, kiosk and online ordering."},
{"id": "connect_pos", "title": "Set up Upvendo POS", "route": "/device-management/devices", "note": "First-party — no third-party POS is connected. Add and activate the POS device here. Registers live in a SEPARATE 'POS' nav group — /pos/registers — which stays hidden until the merchant actually carries the first-party marker (firstPartyPosOnly gate). Register passcodes are NOT there: they live on the person, in Settings -> Team -> Users -> 'POS access' (/settings/team/users). The old /pos/staff roster was retired on 2026-07-30 and now redirects there."},
{"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 the menu, kiosk and online ordering — set this up before the merchant goes live / takes live payments. It powers kiosk and online-ordering payments. At the Upvendo POS counter itself, settlement is cash-only on production."},
{"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only."}
]
}Notes on Verification
Verified against upvendo-backend and upvendo-backoffice on origin/production.
- The gate —
config/pos-providers.php:51-52('active' => true, 'test_only' => true);ResellerService.php:675and:731(thetest_onlycheck ahead of thefirst_partybypass at:682/:736);FirstPartyPosProvisioningService.php:70-77(the 403 for a live merchant);SelectPosProviderRequest.php:33(first_partyandactive);ResellerController.php:608(first-party stripped from the reseller-assignment picker). - Reseller mechanics —
Reseller.php:44/:137(pos_providers),ResellerController::createMerchantandsendMerchantCredentials(routesresellers.php:54-55),/clients+/clients/:idpages andClientFormDialog.vue, Emily client mode (EmilyWidget.vue:234). These are the standard reseller surface — see reseller/onboarding/square.md for the same mechanics described against a third-party provider that a merchant can actually select today. - First-party selection (when enabled) —
PosProviderController::select→FirstPartyPosProvisioningService::selectFirstPartyPos()writespos_provider = 'upvendo'on the merchant and mirrors it onto everyvendor_id-scoped location; it creates noThirdPartyIntegration. Every other first-party POS route additionally carries thefirst-party-posmiddleware (EnsureFirstPartyPos), which 403s any merchant without the marker — so nothing downstream of selection is reachable either. - Not verified here: the register/staff-PIN management UI in the
upvendo-posiPad app, and kiosk/storefront rendering.