Appearance
Shopcaisse Onboarding (Reseller)
Overview
This is the reseller path for onboarding a merchant onto the Shopcaisse POS (Shopcaisse is also marketed as EasyShop). Shopcaisse is one of Upvendo's in-house, location-scoped channels — the connect step itself is identical to the standard merchant flow; what differs in the reseller context is who creates and operates the merchant account.
The integration is in-house, read-only inbound: Upvendo pulls the existing catalog out of Shopcaisse and pushes paid orders back. The catalog must already exist in Shopcaisse; Upvendo cannot author it.
Production status: Shopcaisse is marked
test_onlyin Upvendo's POS provider config (config/pos-providers.php) and is absent fromPRODUCTION_POS_PROVIDERS(which lists Upvendo POS, Hendrickx and Vanhoutte); Shopcaisse is listed only in dev/testing. Consult the Upvendo team before using it in production.
For the full connect mechanics, catalog mapping, order forwarding, webhooks, and troubleshooting, see the shared integration doc: Shopcaisse Integration Setup. This page covers only the reseller-onboarding specifics.
How a reseller onboards a merchant
Upvendo has a real reseller / partner tier (roles reseller-owner, reseller-administrator, reseller-rep; access level LEVEL_RESELLER in app/Constants/Roles.php). A reseller does not connect Shopcaisse to "their own" account — they create a merchant account and then run the standard guided setup against that merchant.
1. Create the merchant account
A partner admin creates the merchant via POST /back-office/partner/merchants (ResellerController::createMerchant → ResellerService::createMerchant, app/Services/BackOffice/ResellerService.php:541). This:
- Creates the merchant + owner user (
MerchantService::create) from the partner-supplied name, email, locale and an initial password. - Stamps the merchant with
reseller_id,onboarding_type: 'reseller', andmerchant_status: New. - Sets the owner's
must_change_password: true. - Appends the new merchant to the partner user's
vendor_ids(so the reseller can act on it). - Initializes the merchant's Cloudflare D1 tables.
Optionally, POST /back-office/partner/merchants/{merchantId}/send-credentials (ResellerController::sendMerchantCredentials) regenerates a temporary password and emails the merchant their login credentials.
There is no public reseller self-signup. Resellers themselves are created by a global administrator (
POST /back-office/admin/resellers, Global-Admin only). Public registration (routes/api/guest.php) only supportsUserTypes::Merchant/UserTypes::Representative.
2. Act as the merchant
To operate the merchant, the reseller switches context with POST /back-office/set-merchant (AuthService::setMerchant), which re-issues a JWT scoped to that merchant and switches the tenant DB connection. Resellers can only switch to merchants in their own vendor_ids. (There is no separate "impersonate" endpoint — context-switch is JWT re-scoping.) Allowed roles: GLOBAL_ADMINISTRATOR, RESELLER_ADMINISTRATOR, RESELLER_OWNER (SetMerchantRequest).
3. Run guided setup for the merchant
With the merchant context selected, the reseller runs the same guided setup as a merchant would. Two reseller-specific behaviors apply:
- Emily uses the reseller endpoint. When the user is a partner, the Emily onboarding widget talks to
emily-resellerinstead ofemily, and the partner-dashboard "add merchant" entry prompts "Let's add a new merchant! … Which POS system does the merchant use?" (EmilyWidget.vue). - POS choices are limited to the reseller's assigned providers. The in-house channel picker filters to the reseller's
reseller_providers(AddInhouseChannelDialog.vue:81-92), and Emily's onboarding restricts POS options to the reseller's supported providers (getMerchantResellerProviders,currentResellerPosProviders). So Shopcaisse only appears if it is one of that reseller's enabled providers.
The reseller portal, partner dashboard, commission/billing, and Stripe Connect surfaces exist (
routes/api/backoffice/resellers.php,pages/partner/*,pages/clients/*) but are outside the scope of this onboarding doc.
Connect mechanics (same as the merchant flow)
- Single JWT bearer token, merchant-level "paste once". The reseller pastes one Shopcaisse JWT into the connect dialog (
ShopcaisseChannelDialog.vue— a single token field, no manual store-picker). Upvendo authenticates the token (GET /v1/authentication), then creates one Upvendo Location per Shopcaisse store the token covers, recording astore_location_map, and dispatches a catalog sync per mapped location (ShopCaisseIntegrationService::syncLocations). - Required token permissions:
store.*.orders.write(push orders) andcompany.*.items.read(read catalog); missing either fails with "Missing required permissions: store.*.orders.write and company.*.items.read". Optionalcompany.*.items.writeadditionally allows pushing simple items. - API environment is decided by the token's
namespaceclaim (prod→api.shop-caisse.com,staging→api-staging.shop-caisse.com), not a manual setting. - In-house exclusivity: only one in-house channel may be active per merchant — adding Shopcaisse when another in-house channel exists is rejected with "Another in-house channel is already enabled for this merchant" (
ThirdPartyIntegrationHelper). - Catalog is read-only. Creating modifier groups, modifiers, or categories is blocked. A simple item can be created and pushed back to Shopcaisse only if its VAT resolves and the token has
company.*.items.write. On synced items, name, kitchen_name, modifier_group_ids, plu, tax_rate_code are locked; price (and description) are editable and push back to Shopcaisse. - Only a branding profile is enforced at connect (
ThirdPartyIntegrationHelper::checkRequiredProfiles). Payment and billing profiles are optional at connect time (set them up so customers can pay — a terminal provider for Kiosk, Stripe for Online Ordering). - Disconnect is disabled. The backoffice delete endpoint returns "Disconnecting ShopCaisse integration is disabled. Please contact administrator." — contact Upvendo support to disconnect.
- Loyalty reward redemption is not supported. Shopcaisse-connected locations cannot redeem loyalty rewards on Kiosk or Online Ordering — the redemption is blocked for both channels (
config/pos-providers.phpsupports_loyalty_redemption => false;Location::supportsLoyaltyRedemption()is default-deny, Shopcaisse remains in a pending loyalty-integration state).
Merchant-side Shopcaisse prerequisites
Before connecting, the merchant's Shopcaisse account must be ready. The only requirement Upvendo's code can confirm is that the catalog already exists in Shopcaisse (items, modifiers, VAT codes) — Upvendo reads it from there and cannot author it — and that the token grants the required permissions above.
The following Shopcaisse-side conditions are referenced by Upvendo error handling but are configured and enforced inside Shopcaisse, so the exact tiers, pricing, and UI are not verified from these repos — confirm current details with Shopcaisse / the reseller:
- A Public API / External Application must be available so the merchant (or reseller) can create an application and obtain the JWT bearer token, and set its webhook URL to Upvendo's
…/api/webhook/shopcaisse. - An Order Management Module must be active on the POS device(s) that will receive orders, or Shopcaisse rejects order creation with an "…does not have an Order Management Module license…" error.
- The Shopcaisse POS device must be set up, logged in and online to accept incoming orders.
Subscription pricing, per-device licensing details, the iPad "Discovery Mode" gate, and the exact Shopcaisse backoffice navigation path are not verified here — they live entirely in Shopcaisse, not in Upvendo's code.
Discovery Flow (JSON - for deterministic parsing)
discovery_questions
[
{
"id": "channels",
"text": "Which channels for this merchant?",
"text_i18n": {
"nl": "Welke kanalen voor deze handelaar?",
"fr": "Quels canaux pour ce marchand ?",
"de": "Welche Kanäle für diesen Händler?",
"es": "¿Qué canales para este comercio?",
"it": "Quali canali per questo commerciante?",
"pt": "Que canais para este comerciante?"
},
"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"
}
}
},
{
"id": "prerequisites_confirmed",
"text": "Have you confirmed with the merchant that the Shopcaisse-side prerequisites are met (Public API / External Application available so a JWT can be created, the token grants company.*.items.read + store.*.orders.write, Order Management Module active on the order-receiving POS device(s), and the POS device online)?",
"text_i18n": {
"nl": "Heb je met de merchant bevestigd dat aan de Shopcaisse-vereisten is voldaan (Public API / External Application beschikbaar zodat een JWT kan worden aangemaakt, de token verleent company.*.items.read + store.*.orders.write, Order Management Module actief op de order-ontvangende kassa-apparaten, en het kassa-apparaat online)?",
"fr": "Avez-vous confirmé avec le marchand que les prérequis côté Shopcaisse sont remplis (API publique / application externe disponible pour créer un JWT, le jeton accorde company.*.items.read + store.*.orders.write, module Order Management actif sur les appareils de caisse qui reçoivent les commandes, et appareil de caisse en ligne) ?",
"de": "Haben Sie mit dem Händler bestätigt, dass die Shopcaisse-seitigen Voraussetzungen erfüllt sind (Public API / External Application verfügbar, damit ein JWT erstellt werden kann, das Token gewährt company.*.items.read + store.*.orders.write, Order-Management-Modul auf den bestellungsempfangenden Kassengeräten aktiv und das Kassengerät online)?",
"es": "¿Has confirmado con el comercio que se cumplen los requisitos del lado de Shopcaisse (API pública / aplicación externa disponible para crear un JWT, el token concede company.*.items.read + store.*.orders.write, módulo Order Management activo en los dispositivos TPV que reciben pedidos, y el dispositivo TPV en línea)?",
"it": "Hai confermato con il commerciante che i prerequisiti lato Shopcaisse sono soddisfatti (API pubblica / applicazione esterna disponibile per creare un JWT, il token concede company.*.items.read + store.*.orders.write, modulo Order Management attivo sui dispositivi POS che ricevono gli ordini e dispositivo POS online)?",
"pt": "Confirmou com o comerciante que os pré-requisitos do lado do Shopcaisse estão cumpridos (API pública / aplicação externa disponível para criar um JWT, o token concede company.*.items.read + store.*.orders.write, módulo Order Management ativo nos dispositivos POS que recebem pedidos e o dispositivo POS online)?"
},
"options": [
"Yes, all confirmed",
"Not yet"
],
"labels": {
"nl": {
"Yes, all confirmed": "Ja, alles bevestigd",
"Not yet": "Nog niet"
},
"fr": {
"Yes, all confirmed": "Oui, tout est confirmé",
"Not yet": "Pas encore"
},
"de": {
"Yes, all confirmed": "Ja, alles bestätigt",
"Not yet": "Noch nicht"
},
"es": {
"Yes, all confirmed": "Sí, todo confirmado",
"Not yet": "Todavía no"
},
"it": {
"Yes, all confirmed": "Sì, tutto confermato",
"Not yet": "Non ancora"
},
"pt": {
"Yes, all confirmed": "Sim, tudo confirmado",
"Not yet": "Ainda não"
}
},
"abort_if": "Not yet",
"abort_message": "Confirm the Shopcaisse-side prerequisites first. Without the Public API / External Application no JWT can be created; without company.*.items.read + store.*.orders.write the connect call fails with 'Missing required permissions'; without an active Order Management Module on the POS device order creation is rejected. The exact Shopcaisse subscription/licensing details are configured in Shopcaisse — verify them with the merchant or reseller.",
"abort_message_i18n": {
"nl": "Bevestig eerst de Shopcaisse-vereisten. Zonder de Public API / External Application kan er geen JWT worden aangemaakt; zonder company.*.items.read + store.*.orders.write mislukt de connect-aanroep met 'Missing required permissions'; zonder een actieve Order Management Module op het kassa-apparaat wordt het aanmaken van orders geweigerd. De exacte Shopcaisse-abonnements-/licentiedetails worden in Shopcaisse geconfigureerd — verifieer ze met de merchant of reseller.",
"fr": "Confirmez d'abord les prérequis côté Shopcaisse. Sans l'API publique / application externe, aucun JWT ne peut être créé ; sans company.*.items.read + store.*.orders.write, l'appel de connexion échoue avec « Missing required permissions » ; sans module Order Management actif sur l'appareil de caisse, la création de commande est rejetée. Les détails exacts d'abonnement/licence Shopcaisse sont configurés dans Shopcaisse — vérifiez-les avec le marchand ou le revendeur.",
"de": "Bestätigen Sie zuerst die Shopcaisse-seitigen Voraussetzungen. Ohne die Public API / External Application kann kein JWT erstellt werden; ohne company.*.items.read + store.*.orders.write schlägt der Connect-Aufruf mit „Missing required permissions“ fehl; ohne aktives Order-Management-Modul auf dem Kassengerät wird die Bestellerstellung abgelehnt. Die genauen Shopcaisse-Abonnement-/Lizenzdetails werden in Shopcaisse konfiguriert — überprüfen Sie sie mit dem Händler oder Reseller.",
"es": "Confirma primero los requisitos del lado de Shopcaisse. Sin la API pública / aplicación externa no se puede crear ningún JWT; sin company.*.items.read + store.*.orders.write la llamada de conexión falla con «Missing required permissions»; sin un módulo Order Management activo en el dispositivo TPV se rechaza la creación de pedidos. Los detalles exactos de suscripción/licencia de Shopcaisse se configuran en Shopcaisse — verifícalos con el comercio o el distribuidor.",
"it": "Conferma prima i prerequisiti lato Shopcaisse. Senza l'API pubblica / applicazione esterna non è possibile creare alcun JWT; senza company.*.items.read + store.*.orders.write la chiamata di connessione fallisce con «Missing required permissions»; senza un modulo Order Management attivo sul dispositivo POS la creazione dell'ordine viene rifiutata. I dettagli esatti di abbonamento/licenza Shopcaisse sono configurati in Shopcaisse — verificali con il commerciante o il rivenditore.",
"pt": "Confirme primeiro os pré-requisitos do lado do Shopcaisse. Sem a API pública / aplicação externa não é possível criar um JWT; sem company.*.items.read + store.*.orders.write a chamada de ligação falha com «Missing required permissions»; sem um módulo Order Management ativo no dispositivo POS a criação de pedidos é rejeitada. Os detalhes exatos de subscrição/licenciamento do Shopcaisse são configurados no Shopcaisse — verifique-os com o comerciante ou revendedor."
}
},
{
"id": "menu_ready",
"text": "Does the merchant have their menu set up in Shopcaisse?",
"text_i18n": {
"nl": "Heeft de handelaar het menu ingesteld in Shopcaisse?",
"fr": "Le marchand a-t-il configuré son menu dans Shopcaisse ?",
"de": "Hat der Händler sein Menü in Shopcaisse eingerichtet?",
"es": "¿El comercio tiene su menú configurado en Shopcaisse?",
"it": "Il commerciante ha configurato il proprio menu in Shopcaisse?",
"pt": "O comerciante tem o menu configurado no Shopcaisse?"
},
"options": [
"Yes, menu is in Shopcaisse",
"No, not yet"
],
"labels": {
"nl": {
"Yes, menu is in Shopcaisse": "Ja, menu staat in Shopcaisse",
"No, not yet": "Nee, nog niet"
},
"fr": {
"Yes, menu is in Shopcaisse": "Oui, le menu est dans Shopcaisse",
"No, not yet": "Non, pas encore"
},
"de": {
"Yes, menu is in Shopcaisse": "Ja, das Menü ist in Shopcaisse",
"No, not yet": "Nein, noch nicht"
},
"es": {
"Yes, menu is in Shopcaisse": "Sí, el menú está en Shopcaisse",
"No, not yet": "No, todavía no"
},
"it": {
"Yes, menu is in Shopcaisse": "Sì, il menu è in Shopcaisse",
"No, not yet": "No, non ancora"
},
"pt": {
"Yes, menu is in Shopcaisse": "Sim, o menu está no Shopcaisse",
"No, not yet": "Não, ainda não"
}
},
"abort_if": "No, not yet",
"abort_message": "The merchant needs to set up their menu in Shopcaisse first. Shopcaisse only supports pulling menu items from the POS - items cannot be created in Upvendo (only simple items can be pushed back if the token grants company.*.items.write). Please have them create their menu in Shopcaisse, then restart the onboarding.",
"abort_message_i18n": {
"nl": "De handelaar moet het menu eerst in Shopcaisse instellen. Shopcaisse ondersteunt alleen het ophalen van menu-items uit de kassa — items kunnen niet in Upvendo worden aangemaakt (alleen eenvoudige items kunnen worden teruggestuurd als de token company.*.items.write verleent). Laat hen hun menu in Shopcaisse aanmaken en start daarna de onboarding opnieuw.",
"fr": "Le marchand doit d'abord configurer son menu dans Shopcaisse. Shopcaisse ne prend en charge que la récupération des articles depuis la caisse — les articles ne peuvent pas être créés dans Upvendo (seuls des articles simples peuvent être renvoyés si le jeton accorde company.*.items.write). Demandez-lui de créer son menu dans Shopcaisse, puis recommencez l'intégration.",
"de": "Der Händler muss sein Menü zuerst in Shopcaisse einrichten. Shopcaisse unterstützt nur das Abrufen von Artikeln aus dem Kassensystem — Artikel können nicht in Upvendo erstellt werden (nur einfache Artikel können zurückgeschrieben werden, wenn das Token company.*.items.write gewährt). Lassen Sie ihn sein Menü in Shopcaisse anlegen und starten Sie dann das Onboarding neu.",
"es": "El comercio debe configurar primero su menú en Shopcaisse. Shopcaisse solo admite obtener artículos desde el TPV — los artículos no se pueden crear en Upvendo (solo se pueden devolver artículos simples si el token concede company.*.items.write). Pídele que cree su menú en Shopcaisse y luego reinicia el onboarding.",
"it": "Il commerciante deve prima configurare il proprio menu in Shopcaisse. Shopcaisse supporta solo il recupero degli articoli dal POS — gli articoli non possono essere creati in Upvendo (solo articoli semplici possono essere rimandati indietro se il token concede company.*.items.write). Chiedigli di creare il menu in Shopcaisse e poi riavvia l'onboarding.",
"pt": "O comerciante precisa de configurar primeiro o menu no Shopcaisse. O Shopcaisse só permite obter artigos a partir do POS — os artigos não podem ser criados no Upvendo (apenas artigos simples podem ser reenviados se o token conceder company.*.items.write). Peça-lhe para criar o menu no Shopcaisse e depois reinicie o onboarding."
}
}
]setup_steps
{
"Kiosk only": [
{"id": "payment_profile", "title": "Set up Payment Profile", "route": "/settings/payments", "optional_for_go_live": true},
{"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true},
{"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand"},
{"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
{"id": "select_location", "title": "Select Location from Topbar", "action": "open_location_selector"},
{"id": "connect_pos", "title": "Connect Shopcaisse", "route": "/shopcaisse", "channel": "shopcaisse"},
{"id": "sync_menu", "title": "Sync Menu", "route": "/shopcaisse"}
],
"Online ordering only": [
{"id": "payment_profile", "title": "Set up Payment Profile", "route": "/settings/payments", "optional_for_go_live": true},
{"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true},
{"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand"},
{"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
{"id": "select_location", "title": "Select Location from Topbar", "action": "open_location_selector"},
{"id": "connect_pos", "title": "Connect Shopcaisse", "route": "/shopcaisse", "channel": "shopcaisse"},
{"id": "sync_menu", "title": "Sync Menu", "route": "/shopcaisse"},
{"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": "test_online_ordering", "title": "Open Online Ordering URL & Test Order", "route": "/online/online-ordering"}
],
"Both": [
{"id": "payment_profile", "title": "Set up Payment Profile", "route": "/settings/payments", "optional_for_go_live": true},
{"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true},
{"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand"},
{"id": "create_location", "title": "Create Location", "route": "/settings/locations"},
{"id": "select_location", "title": "Select Location from Topbar", "action": "open_location_selector"},
{"id": "connect_pos", "title": "Connect Shopcaisse", "route": "/shopcaisse", "channel": "shopcaisse"},
{"id": "sync_menu", "title": "Sync Menu", "route": "/shopcaisse"},
{"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": "test_online_ordering", "title": "Open Online Ordering URL & Test Order", "route": "/online/online-ordering"}
]
}The base (Kiosk-only) steps above mirror
BASE_SETUP_STEPS.shopcaissein the backoffice (src/components/dialogs/guided-setup/types.ts): Payment Profile → Billing Profile → Store Branding → Create Location → Select Location from Topbar (open_location_selector) → Connect Shopcaisse (/shopcaisse) → Sync Menu (/shopcaisse). The online-ordering steps come fromONLINE_ORDERING_CHANNEL_STEPSand use/online-settingsand/online/online-ordering. When Emily supplies dynamicsetup_steps, those take priority over this fallback.