Skip to content

Square Onboarding (Reseller)

Overview

How a reseller (partner) onboards a client merchant who uses Square as their POS. There are two distinct phases:

  1. Reseller-side (partner context): the reseller creates a client merchant from the Clients page and sends login credentials. This is reseller-specific surface that exists in code.
  2. Merchant-side (client context): the Square connection itself is the standard merchant Square flow — there is no reseller-specific Square route. The reseller switches into the client's merchant context ("client mode", backed by the admin-vendor-override middleware) and runs the normal /square OAuth connect, or the merchant does it themselves after logging in.

Key facts (code-verified):

  • A reseller is a first-class entity (Reseller model, resellers collection). Reseller users hold the reseller-owner / reseller-administrator / reseller-rep roles; the reseller-administrator display name is "Partner Administrator". A reseller's merchants are called clients.
  • Client creation does NOT pick a POS provider. ResellerService::createMerchant never references Square. POS selection happens later, inside the client's merchant context.
  • Square availability is per-reseller. A reseller can offer Square to a client only if square is in that reseller's pos_providers array. Emily filters the POS options to the reseller's assigned providers in client mode.
  • No Payment Profile setup — when Square is connected, Square handles kiosk (Square Terminal) and online (Square Web Payments SDK) payments. Reseller-created merchants are forced to stripe_onboarding_completed = true at creation.
  • No prerequisite profile is enforced for Square — unlike the other POS providers, Square is NOT in the branding-required list (ThirdPartyIntegrationHelper::checkRequiredProfiles), so the Square connect path enforces no profile at all. Branding is still recommended because Square does not provide colors, fonts, or logos (only social links are imported).
  • Locations, menu, and inventory sync automatically on first connect — no separate sync step.

See the standard merchant docs for the shared integration mechanics: Square Integration Setup, Square Integration feature, Guided Setup, Payment Profiles.


Reseller-Specific Surface (verified in code)

Creating the client merchant

A Partner Admin (a reseller-administrator / reseller-owner user, or a global admin acting for the reseller) creates a client merchant:

  1. Go to Clients (/clients)
  2. Click Add to open the New Client dialog (ClientFormDialog.vue)
  3. Fill in: First name, Last name, Email, Password, optional Phone, optional Business name, Country/locale, Default language. (There is no POS-provider or owner-name field here.)
  4. Save — this POSTs to POST /back-office/partner/merchants (ResellerController::createMerchant)

What ResellerService::createMerchant sets on the new merchant:

  • reseller_id = the partner's reseller
  • onboarding_type = reseller
  • merchant_status = New
  • stripe_onboarding_completed = true (forced)
  • the owner user's must_change_password = true
  • appends the new merchant to the partner user's vendor_ids

It calls the shared MerchantService::create and runs the Cloudflare D1 initialize_merchant_tables migration. The request is authorized by PermissionService::isGlobalUser (true for reseller-owner / reseller-administrator / global-administrator).

Sending credentials

From the client detail page (/clients/:id, route name clients-id), Send Credentials issues a temporary password to the merchant owner: POST /back-office/partner/merchants/{merchantId}/send-credentials (ResellerController::sendMerchantCredentials). A reseller can only send credentials for a merchant whose reseller_id matches their own.

Which POS providers the client can choose

The reseller's pos_providers array (on the Reseller model) controls the POS options surfaced to its clients. ResellerService::getMerchantResellerProviders filters config('pos-providers.php') down to the reseller's assigned keys (dropping inactive / test-only providers in production). Square is offered to the client only if square is in the reseller's pos_providers. Square is a defined provider in config/pos-providers.php (key: square, countries US/CA/GB/AU/JP/IE/FR/ES) but is not auto-included for resellers — it must be assigned per reseller (managed by a global admin on the reseller record).

Onboarding the client via Emily ("client mode")

When the partner operates in a specific client's context, Emily runs in client mode (store.getters.getMerchantMode; the Emily endpoint resolves to emily-merchant). This is the same full merchant onboarding flow, scoped to the client merchant. In client mode the POS-selection screen always shows (even for a single provider) and is filtered to the reseller's pos_providers.

Not verified here: Emily/proxy onboarding-state internals, greeting copy, and the partner-dashboard "open onboarding" event live in the SPA/proxy and were only inspected at the component level.

Connecting Square (same as a standalone merchant)

There is no reseller-specific Square connect route. Whether the reseller switches into the client's context (client mode / admin-vendor-override) or the merchant logs in directly, the connect is the standard flow:

  • Backoffice page Square (/square) → Connect with Square
  • Frontend dispatch → POST /back-office/square/oauth (SquareIntegrationController::initiateOAuth)
  • On success, redirect to /square/success and a full sync (locations, menu, inventory) starts automatically

Discovery Flow (JSON - for deterministic parsing)

discovery_questions
[
  {
    "id": "channels",
    "text": "Which channels for this client merchant?",
    "text_i18n": {
      "nl": "Welke kanalen voor deze klant?",
      "fr": "Quels canaux pour ce marchand client ?",
      "de": "Welche Kanäle für diesen Kunden?",
      "es": "¿Qué canales para este comercio cliente?",
      "it": "Quali canali per questo commerciante cliente?",
      "pt": "Que canais 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"
      }
    }
  },
  {
    "id": "menu_ready",
    "text": "Does the merchant have their menu set up in Square?",
    "text_i18n": {
      "nl": "Heeft de handelaar het menu ingesteld in Square?",
      "fr": "Le marchand a-t-il configuré son menu dans Square ?",
      "de": "Hat der Händler sein Menü in Square eingerichtet?",
      "es": "¿El comercio tiene su menú configurado en Square?",
      "it": "Il commerciante ha configurato il proprio menu in Square?",
      "pt": "O comerciante tem o menu configurado no Square?"
    },
    "options": [
      "Yes, menu is in Square",
      "No, will create in Upvendo"
    ],
    "labels": {
      "nl": {
        "Yes, menu is in Square": "Ja, menu staat in Square",
        "No, will create in Upvendo": "Nee, wordt aangemaakt in Upvendo"
      },
      "fr": {
        "Yes, menu is in Square": "Oui, le menu est dans Square",
        "No, will create in Upvendo": "Non, sera créé dans Upvendo"
      },
      "de": {
        "Yes, menu is in Square": "Ja, das Menü ist in Square",
        "No, will create in Upvendo": "Nein, wird in Upvendo erstellt"
      },
      "es": {
        "Yes, menu is in Square": "Sí, el menú está en Square",
        "No, will create in Upvendo": "No, se creará en Upvendo"
      },
      "it": {
        "Yes, menu is in Square": "Sì, il menu è in Square",
        "No, will create in Upvendo": "No, sarà creato in Upvendo"
      },
      "pt": {
        "Yes, menu is in Square": "Sim, o menu está no Square",
        "No, will create in Upvendo": "Não, será criado no Upvendo"
      }
    }
  }
]
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; it 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 (POST /back-office/partner/merchants/{merchantId}/send-credentials). 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, not enforced for Square. Square imports social links but not colors/fonts/logo."},
    {"id": "connect_pos", "title": "Connect Square", "route": "/square", "channel": "square", "note": "Standard merchant Square OAuth (POST /back-office/square/oauth). Locations, menu, and inventory import automatically. Available only if the reseller's pos_providers includes 'square'."},
    {"id": "complete_location", "title": "Review Imported Locations", "route": "/settings/locations", "note": "Most fields are pre-filled from Square. Verify details and add delivery zones if needed."},
    {"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"},
    {"id": "kiosk_device", "title": "Set up Kiosk and Pair Square Terminal", "route": "/device-management/devices", "note": "Generate a device code in Upvendo and enter it on the Square Terminal; the device.code.paired webhook confirms pairing."},
    {"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only; not customer payments (Square handles those). 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, not enforced for Square. Square imports social links but not colors/fonts/logo."},
    {"id": "connect_pos", "title": "Connect Square", "route": "/square", "channel": "square", "note": "Standard merchant Square OAuth (POST /back-office/square/oauth). Locations, menu, and inventory import automatically. Available only if the reseller's pos_providers includes 'square'."},
    {"id": "complete_location", "title": "Review Imported Locations", "route": "/settings/locations", "note": "Most fields are pre-filled from Square. Verify details and add delivery zones if needed."},
    {"id": "online_settings", "title": "Set up Online Settings", "route": "/online-settings"},
    {"id": "configure_online_ordering", "title": "Set up Online Ordering", "route": "/online/online-ordering", "note": "Online payments use the Square Web Payments SDK — no Stripe or Viva Wallet setup needed."},
    {"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only; not customer payments."}
  ],
  "Both": [
    {"id": "branding_profile", "title": "Set up Store Branding", "route": "/settings/brand", "optional_for_go_live": true, "note": "Recommended, not enforced for Square. Square imports social links but not colors/fonts/logo."},
    {"id": "connect_pos", "title": "Connect Square", "route": "/square", "channel": "square", "note": "Standard merchant Square OAuth (POST /back-office/square/oauth). Locations, menu, and inventory import automatically. Available only if the reseller's pos_providers includes 'square'."},
    {"id": "complete_location", "title": "Review Imported Locations", "route": "/settings/locations", "note": "Most fields are pre-filled from Square. Verify details and add delivery zones if needed."},
    {"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"},
    {"id": "kiosk_device", "title": "Set up Kiosk and Pair Square Terminal", "route": "/device-management/devices", "note": "Generate a device code in Upvendo and enter it on the Square Terminal; the device.code.paired webhook confirms pairing."},
    {"id": "online_settings", "title": "Set up Online Settings", "route": "/online-settings"},
    {"id": "configure_online_ordering", "title": "Set up Online Ordering", "route": "/online/online-ordering", "note": "Online payments use the Square Web Payments SDK — no Stripe or Viva Wallet setup needed."},
    {"id": "billing_profile", "title": "Set up Billing Profile", "route": "/settings/billing", "optional_for_go_live": true, "note": "Optional. Upvendo platform invoicing only; not customer payments."}
  ]
}

Notes on Verification

  • Reseller mechanics (Reseller model, roles, ResellerService::createMerchant, ResellerController, pos_providers, /clients pages, Emily client mode) are confirmed in upvendo-backend and upvendo-backoffice.
  • Square mechanics (OAuth scopes, provider_integrated = square, isSquareIntegrated() payment override, no Square prerequisite profile, location/menu/inventory sync, webhooks) are confirmed and detailed in shared/integrations/square.md.
  • Route corrections applied: the online-ordering route is /online/online-ordering (not /pickup-and-delivery/online-ordering, which does not exist); /online-settings is a real precursor step. A /orders "Go Live" route does not exist in the backoffice and has been removed.
  • Reseller billing / commissions / payout settings (platform-collects vs reseller-collects, Stripe Connect, commission ledger, payout statements) exist in code but are a separate subsystem from POS onboarding and are out of scope for this Square onboarding doc.
  • Not verified here: Square Web Payments SDK storefront/proxy tokenization, Emily/proxy onboarding-state internals, and email-delivery of credentials live outside these two repos.