Skip to content

Upvendo POS Onboarding (Reseller)

Overview

Upvendo POS is staged — only test-flagged merchants can activate it in production.config/pos-providers.php:51-52 sets 'active' => true, 'test_only' => true on the upvendo provider, and the test_only check runs before the first_party bypass in both branches of ResellerService::getMerchantResellerProviders() (ResellerService.php:675 and :731): a client merchant sees the Upvendo POS tile only if the merchant record is flagged is_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 drops test_only from 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:

  1. You cannot put Upvendo POS in a reseller's catalog. A reseller's provider list is the pos_providers array on the Reseller record (Reseller.php:44, read via getPosProviders() at :137), set by a global admin from a picker fed by GET /back-office/pos-providers (ResellerController::getPosProviders, route resellers.php:9). That endpoint strips every first-party provider before returning (ResellerController.php:608) — first-party is not a reseller-sold product — so upvendo never appears in the picker and cannot be assigned from the back office. Note this is a picker guarantee, not a validation one: StoreResellerRequest and UpdateResellerRequest validate pos_providers.* as a bare string with no allow-list, so a hand-crafted API call can still write upvendo onto 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.
  2. Even if upvendo were in pos_providers, listing is decided by the merchant's flag. In the reseller branch of getMerchantResellerProviders() the active check runs first (ResellerService.php:726), then the test_only check against the client merchant'sis_test flag (:731), before both the first_party bypass (: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).
  3. A direct API call for a live merchant is rejected. POST /api/back-office/pos/select-provider { provider: "upvendo" } passes SelectPosProviderRequest validation (the provider is first_party and active), but FirstPartyPosProvisioningService::selectFirstPartyPos() throws a 403 "Unsupported POS provider." when the target merchant is not is_test in production while test_only is set (FirstPartyPosProvisioningService.php:70-77). This holds in client mode too: acting for a client merchant only adds ?vendor_id= for the admin-vendor-override middleware (pos.php:40), which rebinds which merchant is targeted — the gate then evaluates that merchant's is_test flag.

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:

  1. 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.
  2. 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 with first_party: true to client merchants of every reseller in every country — the first_party flag bypasses the reseller-key and country filters (ResellerService.php:736) — subject to the test_only gate: in production only client merchants flagged is_test receive it until test_only is dropped. It is not scoped by pos_providers the 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() writes pos_provider = 'upvendo' on the merchant and mirrors it onto every location the merchant owns; it creates no ThirdPartyIntegration record.
  • 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.php is labelled the walking skeleton; SettleOrderRequest accepts only amount_tendered in 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::checkRequiredProfiles branding 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.

  1. Go to Clients (/clientssrc/pages/clients/index.vue)
  2. Open the Add New Client dialog (ClientFormDialog.vue)
  3. 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 to Belgium) and Default Language* (defaults to English). There is no POS-provider field here, and no billing section — the billing_section / billing_company_name / billing_vat keys in the locale files are dead, used by no component.
  4. Save — this POSTs to POST /back-office/partner/merchants (ResellerController::createMerchant, route resellers.php:54). The caller's own reseller_id is 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 the active gate 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 a menu_ready answer 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 gateconfig/pos-providers.php:51-52 ('active' => true, 'test_only' => true); ResellerService.php:675 and :731 (the test_only check ahead of the first_party bypass at :682 / :736); FirstPartyPosProvisioningService.php:70-77 (the 403 for a live merchant); SelectPosProviderRequest.php:33 (first_party and active); ResellerController.php:608 (first-party stripped from the reseller-assignment picker).
  • Reseller mechanicsReseller.php:44/:137 (pos_providers), ResellerController::createMerchant and sendMerchantCredentials (routes resellers.php:54-55), /clients + /clients/:id pages and ClientFormDialog.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::selectFirstPartyPosProvisioningService::selectFirstPartyPos() writes pos_provider = 'upvendo' on the merchant and mirrors it onto every vendor_id-scoped location; it creates no ThirdPartyIntegration. Every other first-party POS route additionally carries the first-party-pos middleware (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-pos iPad app, and kiosk/storefront rendering.