Skip to content

Lightspeed K-Series Integration Setup

Looking for guided setup? Start Emily in the backoffice for step-by-step guided setup.

Purpose

The Lightspeed K-Series integration connects Upvendo to Lightspeed Restaurant K-Series, a cloud-based POS system widely used in hospitality. It imports menus, items, modifiers, categories, tax rates, allergens, and item availability/stock from Lightspeed into Upvendo, and pushes completed Upvendo orders (from Kiosk, Online Ordering, and Table QR channels) back to the Lightspeed POS for kitchen preparation and fulfilment. The integration uses OAuth 2.0 (Keycloak) for authentication and receives real-time order-status and item-availability events from Lightspeed via webhooks.

The connection is merchant-scoped (one OAuth authorization covers the whole Lightspeed account), but data is wired up per location: each Upvendo location is mapped to one Lightspeed business location, and menu sync and orders run per mapping.


Key Concepts

OAuth 2.0 (Keycloak)

Lightspeed K-Series authentication runs through Keycloak OIDC, hosted on a separate domain from the API server. There are no API keys for the merchant to manage — they log in to Lightspeed to authorize Upvendo. The flow:

  1. Upvendo builds an authorization URL with client_id, response_type=code, redirect_uri, the OAuth scope list, and an HMAC-signed state parameter.
  2. The state parameter is an HMAC-SHA256-signed payload (vendor_id + timestamp) protecting against CSRF. It is signed and verified with the Lightspeed client_secret.
  3. The merchant is redirected to Lightspeed's Keycloak login and authorizes Upvendo.
  4. Lightspeed redirects back to the callback (/api/oauth/lightspeed/callback, also aliased at /api/lightspeed/callback) with an authorization code and the state.
  5. Upvendo verifies the state HMAC, then exchanges the code for access and refresh tokens. The token exchange sends client_id:client_secret via HTTP Basic Auth in the Authorization header (NOT in the POST body), per Keycloak's requirements.
  6. The business is fetched from /o/op/data/businesses; the first business's ID and name are stored alongside the tokens.
  7. The merchant is redirected to the Locations tab to pair their Upvendo locations with Lightspeed business locations.

Note: This integration does not use PKCE. (PKCE was used during early development and has since been removed.) CSRF protection comes from the HMAC-signed state parameter.

OAuth Scopes

The integration requests four scopes: orders-api (businesses, floors, menus, discounts, orders, payments, rich item data), financial-api (sales, daily financials, tax rates, payment methods), items (item read/write, stock), and offline_access (required to obtain the durable ~40-day refresh token that keeps the background session alive; without it the refresh token expires in minutes). The Staff API (staff-api) is not requested — it was dropped for least-privilege, as no production flow consumes it.

Location Mappings

The OAuth connection is merchant-scoped, but you must map locations before any data flows:

  • You create your Upvendo locations manually first (Lightspeed does not auto-import them).
  • In the Locations tab you map each Upvendo location to one Lightspeed business location (a businessLocationId).
  • Each mapping is one-to-one in both directions (an Upvendo location can be mapped to only one business location, and vice versa).
  • Creating a mapping registers/links the merchant webhook for that business location and kicks off the initial menu sync for that location.
  • Mappings can be paused, resumed, manually re-synced, or removed individually.

Token Management

Access tokens are managed automatically:

  • Proactive refresh — before each authenticated call the access token is refreshed if it is within a 5-minute buffer of expiry, using the stored refresh token.
  • Reactive 401 retry — if an API call still returns 401, the client force-refreshes the token once and replays the request.
  • Refresh failure (invalid_grant) — if the refresh token is dead (revoked, expired, password change), the integration is flagged needs_reauth. The backoffice shows a Reconnect banner and the merchant must re-authorize via OAuth.
  • Daily keep-alive refresh — a scheduled job force-refreshes every active integration once a day. Keycloak K-Series sessions die if not refreshed for 30 days (the offline_access refresh token itself expires after 40), and refreshes otherwise only ever happened as a side effect of API traffic. Without this, a merchant idle for a month — a seasonal closure — came back to a hard invalid_grant and a forced re-auth. Integrations already flagged needs_reauth are skipped.
  • Refreshes are serialized per integration. Keycloak refresh tokens are single-use and rotate on every refresh, so two workers crossing the pre-expiry window used to invalidate each other's token and kick the whole merchant to needs_reauth mid-operation. A refresh now takes a per-integration lock, re-reads under it, and adopts a token another worker already rotated instead of refreshing again.

Payment Provider Requirement

Lightspeed handles in-house POS payments, but Upvendo still needs Stripe or Viva Wallet for customer-facing payments on Kiosk and Online Ordering. Lightspeed does not provide customer payment processing to Upvendo.

Catalog Import (One-Way)

Menu/catalog data flows one-way, Lightspeed → Upvendo. A menu sync (per mapping) imports tax rates, modifier groups, modifiers, categories, items, item availability/stock, and rich content (per-locale descriptions, allergens, images). Lightspeed is treated as the source of truth: entities that disappear from Lightspeed are deactivated/removed in Upvendo. Orders are the only thing Upvendo pushes back to Lightspeed.

Degraded Menu Syncs

A menu sync does not only end in "success" or "error" — it can also finish degraded. That happens when K-Series returned an incomplete picture of your modifier data, and it is a deliberate safety state: the items still import, but modifier groups, modifiers, and production-instruction groups are neither refreshed nor deleted, so a transient K-Series glitch cannot wipe them. The mapping's sync status is set to degraded (instead of success) with the message "Menu synced (degraded): N items — modifier groups not refreshed this sync (incomplete K-Series modifier data)". (Verified: app/Services/BackOffice/LightspeedKSeriesIntegrationService.php lines 4417-4433 and 4452-4457; orphan-purge skip at lines 5778-5794.) See Troubleshooting → "Menu synced (degraded)" for what to do about it.

Available K-Series API Endpoints

The integration uses the following K-Series endpoints (a subset is also exposed through the backoffice/dev API for diagnostics):

Data TypeEndpointNotes
Businesses/o/op/data/businessesReturns businesses, each with businessLocations[]; first business stored on connect
Account Profiles/o/op/data/account-profilesOrder profiles, filterable by businessLocationId / tag code
Floor Plans/o/op/data/{blId}/floorplansTable layout per business location
Menus/o/op/1/menu/listMenu list for a business location
Menu Load/o/op/2/menu/load/{menuId}Full menu tree (items + modifier groups) — v2 is the primary loader; v1 (/o/op/1/menu/load/{menuId}) is the fallback, used only when v2 returns an empty menu. v1 carries no modifier groups, so falling back to it also marks that run degraded rather than a success (see Degraded Menu Syncs)
Modifiers/o/op/1/menu/modifiersProduction instructions / modifiers. This is the only source of production-instruction choices; if the call fails, or comes back without a group an item references, the run is marked degraded
Discounts/o/op/1/menu/discountsAvailable discounts
Item Availability/o/op/1/itemAvailabilityStock availability per SKU per location
Orders (local)/o/op/1/order/localCreate dine-in orders
Orders (toGo)/o/op/1/order/toGoCreate takeaway/delivery orders
Open Checks/o/op/1/order/table/getCheckCurrent open orders/checks
Payment/o/op/1/payApply a payment to a check. Reached from the automated flow when the merchant enables the (default-OFF) create_unpaid_settlement option — the order is pushed unpaid, then settled here when the fulfilment webhook fires; otherwise dev/diagnostic-only
Webhooks/o/wh/1/webhookCreate (PUT), update (POST), get/delete (GET/DELETE), link/unlink locations
Tax Rates/f/finance/{blId}/tax-ratesTax rate codes and multipliers
Payment Methods/f/finance/{blId}/paymentMethodsAvailable POS payment methods
Accounting Groups/f/finance/{blId}/accountingGroupsRevenue accounting groups
Financials/f/finance/{blId}/financials/{from}/{to}Sales for a date range
Daily Financials/f/finance/{blId}/dailyFinancialsSales for the current business day
Tax Breakdown/tp/v1/business-locations/{blId}/tax-breakdownComputes tax breakdown (exposed via dev endpoint only)
Items API/items/v1/itemsList, create, update, delete items
Rich Items/i/richItem/{businessId}Images, descriptions, allergens per SKU
Allergens/i/allergensAll allergen codes with translations

Order Types

Pushed orders use two creation methods, chosen by the Upvendo order's dining option:

  • Local orders (/o/op/1/order/local) — for dine-in orders (include a tableNumber)
  • ToGo orders (/o/op/1/order/toGo) — for takeaway and delivery orders

Actions

Create Locations (Must Be Done First)

Route: /settings/locations

  1. Create your Upvendo location(s) manually.
  2. Lightspeed does not auto-import locations into Upvendo.

Connect via OAuth

Route: /lightspeedSettings tab

  1. Click Connect with Lightspeed.
  2. You are redirected to the Lightspeed (Keycloak) login.
  3. Log in with a Lightspeed Restaurant K-Series account with sufficient permissions.
  4. Authorize Upvendo.
  5. You are redirected back and land on the Locations tab.
  6. The integration stores your access token, refresh token, and business ID automatically.

Map Locations

Route: /lightspeedLocations tab

  1. The tab lists your Lightspeed business locations and your Upvendo locations.
  2. Map each Upvendo location to one Lightspeed business location.
  3. Creating the mapping links the webhook for that business location and starts the initial menu sync.
  4. You can pause, resume, manually re-sync, or remove a mapping at any time. Removing a mapping wipes the K-Series-imported data on that Upvendo location.

Regions (tax-inclusive & tax-exclusive): Lightspeed K-Series supports both tax-inclusive (EU) and tax-exclusive (US / Canada) locations. The tax method is imported automatically from the K-Series taxIncluded flag on each rate (taxIncluded = falsetax_method = Exclusive; trueInclusive), so US / Canada locations map with no manual configuration and no admin override. For exclusive locations, tax is applied on top of the item price and the pushed order total is derived from the K-Series net taxed total (getTotalMoney()).

Verify Connection

Route: /lightspeedStatus tab

  1. Check the Connection Status card.
  2. Click Test Connection to verify API access (it calls /o/op/data/businesses).
  3. Recent activity is shown in the integration logs below the status.

Reconnect (Token Revoked / needs_reauth)

Route: /lightspeedSettings tab (a warning banner also appears when needs_reauth)

  1. Click Reconnect to re-authorize via OAuth.
  2. Existing mappings are preserved and the webhook is healed/re-linked automatically.
  3. A successful re-auth clears the needs_reauth status.

Disconnect Lightspeed K-Series

Production note: Disconnecting is disabled in production. The backoffice Delete button is greyed out (with a "disconnect disabled" tooltip), and the disable endpoint responds with "Disconnecting Lightspeed K-Series integration is disabled. Please contact administrator." outside of test environments. To remove a live integration, contact an administrator. (See Business Rules.)


Business Rules

  • The OAuth connection is merchant-scoped (one authorization per Lightspeed account), but data is wired per location via location mappings. You must create Upvendo locations manually first — Lightspeed does not auto-import them.
  • Each location mapping is one-to-one: an Upvendo location maps to exactly one Lightspeed business location, and a business location can be mapped to only one Upvendo location.
  • A separate payment provider (Stripe or Viva Wallet) is always required for customer-facing payments on Kiosk and Online Ordering. Lightspeed does not handle these.
  • Access tokens refresh automatically (5-minute proactive buffer, plus a one-shot reactive refresh on a 401). If the refresh token is revoked, the integration is flagged needs_reauth and must be reconnected via OAuth.
  • The OAuth state parameter is HMAC-SHA256-signed with the client_secret to prevent CSRF; it is verified on callback. (The integration does not use PKCE.)
  • Catalog data is imported one-way (Lightspeed → Upvendo). Lightspeed is the source of truth: items removed from Lightspeed are deactivated in Upvendo, and removed modifier groups/modifiers are deleted.
  • A menu sync can end degraded rather than successful. A mapping's sync_status.status is set to degraded when K-Series returned incomplete modifier data — any of: the production-instruction dictionary call (/o/op/1/menu/modifiers) failed; that dictionary was missing a production-instruction group an item references, or returned it with no choices; or a menu's v2 load came back empty and the sync fell back to v1 (which carries no modifier groups). In that state items still import, but the modifier / production-instruction orphan purge is skipped and existing item→production-instruction links are preserved, so nothing already imported is deleted. A degraded run still advances the mapping's last_sync. (Verified: app/Services/BackOffice/LightspeedKSeriesIntegrationService.php lines 4126-4133, 4253-4270, 4417-4433, 4452-4457, 4913-4944, 5060-5088, 5778-5794, 7236-7246.)
  • Three consecutive degraded syncs on the same mapping raise one Sentry alert to Upvendo operations; the alert does not re-fire until a clean success resets the streak. (Verified: V1_FALLBACK_ALERT_THRESHOLD = 3 at line 43; streak and alert logic at lines 7209-7234.)
  • Menu sync runs per mapping: immediately on mapping creation and on a manual sync, plus a scheduled catalog/menu sync that runs once daily at 07:01 by default (overridable per merchant via settings.sync_time), dispatching one sync job per active mapping. That time is a local wall-clock read in the merchant's settings.sync_timezone (defaulting to Europe/Brussels), not UTC, so it does not drift across daylight-saving transitions. The in-house auto-sync command is scheduled every 30 minutes, but each tick only checks which mappings are due — it is not a 30-minute sync.
  • Imported tax rates are marked with source = 'lightspeed'; imported items/categories/modifiers carry an external_ids.lightspeed marker. Removing a mapping or disconnecting wipes only these K-Series-sourced records; merchant-authored records are preserved.
  • Webhook events from Lightspeed are received at /api/webhook/lightspeed and verified via HTTP Basic Auth (LIGHTSPEED_WEBHOOK_USERNAME / LIGHTSPEED_WEBHOOK_PASSWORD). There is no HMAC signature on inbound webhooks.
  • The integration subscribes only to order DELIVERED and item SALES_RESTRICTION_UPDATED events; other order/payment statuses are delivered automatically by Lightspeed and handled by status, not by explicit subscription.
  • The integration keeps a rolling activity log (newest first), capped at the standard integration log limit; consecutive duplicate entries are collapsed by updating the latest entry's timestamp.
  • Only Lightspeed Restaurant K-Series is supported. L-Series and R-Series are not compatible.
  • Disconnecting is disabled in production (see the Disconnect action).

Order Flow

Completed, paid Upvendo orders are pushed to Lightspeed K-Series for fulfilment:

text
Customer places order on Kiosk, Online Ordering, or Table QR
            |
Payment captured via Stripe or Viva Wallet
            |
ProcessOrderJob runs; order reaches "complete" status
            |
Location's provider is Lightspeed -> syncOrderToLightspeed(transaction)
            |
Order pushed to Lightspeed K-Series via API:
  - Local order  (/o/op/1/order/local) for dine-in
  - ToGo order   (/o/op/1/order/toGo) for takeaway / delivery
  (payment is embedded in the order payload)
            |
Lightspeed POS receives the order for kitchen preparation
            |
Lightspeed sends an ORDER status webhook back (DELIVERED / CANCELLED / FAILURE ...)
            |
Upvendo updates the order; recoverable failures are requeued/retried

Order Payload Structure

Each order pushed to Lightspeed includes (built in syncOrderToLightspeed / buildOrderPayload):

  • endpointId — the merchant webhook endpoint (upvendo-{merchantId}) for status callbacks
  • businessLocationId — the mapped Lightspeed business location (integer)
  • thirdPartyReference{orderNumber}.{attempt} (the attempt suffix lets retries mint a fresh reference)
  • accountProfileCode — the K-Series order profile, mapped from the order's dining option. Defaults are the built-in profiles (dine-in → dinein, take-away → takeaway, delivery → takeaway), and each location mapping can override them per order type via the Order profiles dialog (Lightspeed → Locations → ⋯ → Order profiles) to point at dedicated custom profiles (e.g. upvdinein / upvtakeout / upvdelivery — these are just a documentation/naming convention; Upvendo does not require or validate any particular code) — the recommended setup, so paid orders stay visible in the POS Orders queue. What Upvendo actually requires of the mapped takeout/delivery profile: deliveryMode = TAKE_AWAY / DELIVERY and completionMode = MANUALLY (so a settled order stays in the production/Orders queue), plus the reduced-rate VAT rule attached. The order profile drives both the VAT rate and the order's POS behaviour (which tab it lands in, and whether it auto-completes when paid) — see Order profiles & VAT below
  • maxTimeToAttemptOrderDeliverToPos — how long K-Series retries delivery to a register before emitting a FAILURE (5 minutes)
  • items[] — each with a resolved sku, quantity, and modifiers as subItems[] (modifier SKU + price)
  • customerInfo — first name, derived last name, email, phone (E.164)
  • deliveryAddress — for delivery orders only (address lines, zip, city)
  • tableNumber — for dine-in orders
  • orderNote — a single order-level note string (K-Series' to-go order schema has no per-line item note field). Upvendo composes it from, joined with '; ': (1) an order-reference segment (order# / queue# / pager id, per the merchant's display setting), (2) the customer's order-level note (allow_order_notes), then (3) each item-level note (allow_notes) attributed with its item name (e.g. Kleine Friet: extra mayo). Because lines can't carry their own note, item notes are flattened here with their item name so they stay actionable in the kitchen. Printed on the kitchen dockets
  • payment — embedded payment (see below)

Payment

The payment is embedded in the order-create payload, not sent as a separate /o/op/1/pay call in the automated flow:

text
payment:
  paymentMethod:              <merchant-mapped tender, else OOPAYMENT>
  paymentAmount:              <net items subtotal (subtotal − discount) + tip>   (NOT the order total)
  tipAmount:                  <tip>   (only when > 0)
  thirdPartyPaymentReference: <order reference>

paymentAmount is the net items subtotal (subtotal − any order discount) plus the tip — never the transaction total. K-Series recomputes the order total from its own catalog for the pushed SKUs (= the items subtotal), so paymentAmount must equal that recomputed figure to balance; sending the full total (which also carries delivery/pickup fees and gift-card purchases, none of which are line items) makes K-Series reject the order as an Overpayment. The tip is declared separately via tipAmount (requires gratuity enabled on the mapped K-Series tender) so K-Series books it as a tip rather than reading it as an overpayment.

paymentMethod is the K-Series tender the merchant mapped to the order's Upvendo payment method, falling back to OOPAYMENT (Online Order Payment) when unmapped. Deferred methods (cash-on-delivery, invoice) omit the payment block entirely — the order is pushed unpaid and settled off-platform.

The standalone applyPayment (/o/op/1/pay) call is reached from the automated webhook-driven flow when the merchant enables the (default-OFF) create_unpaid_settlement option: prepaid, non-deferred, non-dine-in orders are pushed unpaid, then settled via /o/op/1/pay when the fulfilment webhook fires (default trigger READY_FOR_PICKUP, via settings.settle_trigger). With that option off, /o/op/1/pay is dev/diagnostic-only. (This is separate from the deferred cash-on-delivery / invoice case above, where the order is also pushed unpaid but settled off-platform.)

Order profiles & VAT

K-Series keys the VAT rate on the order profile, not on the item alone: each item's tax is resolved from a tax rule whose condition matches the order's accountProfileCode. The rate also depends on the item class (food vs non-alcoholic drink vs alcohol). This is why the same item is taxed differently depending on the dining option (Belgium example, live-confirmed on K-Series receipts):

Dining optionaccountProfileCodeFoodNon-alcoholic drinkAlcohol
Dine-in (eat in)dinein12%21%21%
Take-away / Delivery / Pickup (online)takeaway6%6%21%

Key points:

  • Upvendo only selects the profile (from the dining option) — it does not set tax rates. Each tax rule in K-Series pairs a tax rate (e.g. BTW6) with an Account profile condition (e.g. Take away), per tax profile (food / drinks / alcohol). Whichever profile the order carries decides which rules fire.
  • Who can edit tax rules differs by region. In the EU, tax rates and tax profiles are set up by Lightspeed and cannot be edited in the Back Office: "Only US and Canadian businesses can manage tax rates and tax profiles in the Back Office. In other countries, tax rates and profiles are automatically set up, but you still must assign tax profiles to accounting groups." (Lightspeed — Managing tax settings). US & Canadian merchants can manage tax rules themselves in the Back Office.
  • Recommended setup: dedicated Upvendo order profiles. Out of the box Upvendo uses the built-in dinein / takeaway profiles, which already bill the correct VAT — but the built-in takeaway profile completes orders the moment they are paid, so prepaid online/kiosk orders skip the POS Orders (Order Management) queue and go straight to Receipts. Creating dedicated profiles (example codes: upvdinein, upvtakeout, upvdelivery — the codes are a naming convention only; Upvendo does not require or validate a specific code) with Order completion = "Complete manually in POS" (completionMode = MANUALLY) and the matching deliveryMode (TAKE_AWAY for takeout, DELIVERY for delivery) keeps paid orders visible on the Orders screen for the kitchen, with the right Pickup/Delivery tab. The trade-off: new custom profiles bill the standard rate until tax rules are attached to them — in the EU that is a one-time request to Lightspeed support (they add the reduced-rate rules for the new profile codes, mirroring the built-in Take away profile). The full step-by-step, including a copy-paste email template for Lightspeed, is in the Lightspeed onboarding guide.
  • Per-line VAT still applies — a single order can mix rates (e.g. food at the reduced rate and alcohol at 21%), because each item's tax rule is evaluated against the order profile independently.
  • Always verify before going live: place one small take-away test order and check the receipt shows the reduced rate; if it shows the standard rate, the tax rules have not been attached to the custom profiles yet.

Idempotency & Retries

Each transaction is pushed once: if external_ids.lightspeed is already set, the push is skipped. On a recoverable FAILURE (e.g. register temporarily unreachable), the external ID is cleared, the order is flagged for requeue, and a retry job is scheduled.

  • There is a ceiling. A recoverable failure — including a bare FAILURE with no reason — used to loop push → FAILURE → requeue with no limit. After 5 pushes the order is parked for manual review instead.
  • Stale cancellations no longer duplicate an order on the POS. A redelivered or reordered CANCELLED event used to re-clear external_ids.lightspeed (the push idempotency key) and re-flag the order for requeue even when it had already been re-pushed under a newer attempt — producing a duplicate on the register. The reference's .{attempt} suffix is now compared against the recorded push attempts and stale outcomes are dropped.
  • A reconnect requeue is gated on the location actually being ready for online orders. The connection test only proves OAuth and API reachability, and passed even when no POS register was accepting online orders — so every requeue burned one of the 5 reconnect attempts on a doomed push. The probe now checks Lightspeed's online-order readiness first. Unknown readiness (endpoint error, or a location it cannot map) fails open and the requeue proceeds.
  • Cancel-after-settle raises an alert. An order cancelled after the customer's payment settled requires a manual Stripe refund; that case now raises a monitored error rather than only writing a log line.

Token Lifecycle Details

BehaviourDetail
Proactive refreshAccess token refreshed within a 5-minute buffer of expiry before authenticated calls
Reactive 401 retryOn a 401, the client force-refreshes the token once and replays the request
Refresh-token expiryTracked separately (refresh_token_expires_at); Keycloak rotates the refresh token on each refresh
Refresh failureAn invalid_grant from Keycloak flags the integration needs_reauth and surfaces a Reconnect banner
Daily keep-aliveA scheduled job force-refreshes every active integration daily (skipping needs_reauth ones), so a long-idle merchant does not lose the session — Keycloak kills it after 30 days without a refresh, and the refresh token expires at 40
ConcurrencyRefreshes take a per-integration lock; a worker that loses the race adopts the token the winner rotated rather than refreshing again and invalidating it
Token exchange / refreshUses HTTP Basic Auth (client_id:client_secret) in the Authorization header, not in the POST body

If tokens cannot be refreshed, reconnect via OAuth from the Settings tab.


Webhook Support

Lightspeed K-Series pushes real-time events to Upvendo:

  • Endpoint: POST /api/webhook/lightspeed (public; verified by Basic Auth)
  • Verification: HTTP Basic Auth using LIGHTSPEED_WEBHOOK_USERNAME and LIGHTSPEED_WEBHOOK_PASSWORD. Credentials are compared with constant-time comparison. There is no HMAC signature on inbound webhooks. (If credentials are unset, requests are rejected except in local.)
  • Endpoint registration: A single merchant-level endpoint (upvendo-{merchantId}) is created on Lightspeed, then linked to each mapped business location. Upvendo's webhook payload includes the callback URL (forced to HTTPS) and the Basic Auth credentials, and subscribes to DELIVERED (resource order) and SALES_RESTRICTION_UPDATED (resource item).
  • Events handled:
    • ORDER — branched on status. Delivered-family statuses (SUCCESS, IN_DELIVERY, READY_FOR_PICKUP, CLOSED) mark the order delivered; cancelled-family statuses (CANCELLED, ABANDONED, FAILURE) cancel it (recoverable failures are requeued). Other statuses are logged and ignored.
    • ITEM — item-availability / stock updates (SALES_RESTRICTION_UPDATED). RESTRICTED with an integer count writes stock. NOT_RESTRICTED (count null) means the merchant lifted the restriction, and Upvendo now removes the inventory record rather than ignoring the event: record presence is "stock is tracked", so leaving it behind kept the item capped or 86'd in Upvendo forever. This is the same un-tracking the other POS stock syncs do. (A full menu sync still calls syncItemAvailability() at the end and remains the safety net for a missed event.)
    • PAYMENT — K-Series does send a separate PAYMENT-type webhook on the same endpoint (discriminated by the payload type). It is handled in a dedicated branch that records the payment status/outcome (SUCCESS / FAILURE).
  • Auto-delivered vs subscribed events: PAYMENT events (and order CANCELLED / FAILURE / READY_FOR_PICKUP) are auto-delivered by Lightspeed and must not be explicitly subscribed — subscribing to them 404s. Only order DELIVERED and item SALES_RESTRICTION_UPDATED are explicitly subscribed.
  • Webhook management: Endpoints are created (PUT), updated (POST), retrieved (GET), and deleted (DELETE) via /o/wh/1/webhook; locations are linked/unlinked individually. Disconnecting (in test) or removing the last mapping deletes the merchant endpoint.

Rich Items, Allergens, and Item Management

Lightspeed K-Series provides enriched item data through its Rich Item and Items APIs. During menu sync, rich content is imported into Upvendo (non-fatal — skipped if the Rich Item module is not provisioned for the account).

Rich Item API

  • List rich items — paginated list with images, descriptions, and allergen data per SKU
  • Get by SKU — detailed rich item data for a single product (getRichItem)
  • Upsert — push enriched item data (descriptions, allergens) from Upvendo to Lightspeed (upsertRichItem); item pictures via updateRichItemPicture
  • Picture spec — retrieve upload specifications for product images
  • Locales — read/set which languages are available for item content per business

During import, allergen codes are mapped to Upvendo's allergen/dietary fields, and item images are uploaded to Cloudflare (deduped by source URL).

Allergen Data

The /i/allergens endpoint returns allergen codes with translations. These are mapped to Upvendo's allergen fields when items are enriched.

Items API (CRUD)

The Items API supports list/get/create/update/delete operations (/items/v1/items). The backoffice exposes a read/list endpoint; the underlying client supports full CRUD. This is the catalog item API; rich content lives in the separate Rich Item API.


Prerequisites Checklist

Before connecting Lightspeed K-Series, complete these in order:

TaskRouteRequired
Payment Profile/settings/paymentsYes (Stripe or Viva Wallet)
Billing Profile/settings/billingYes
Branding Profile/settings/brandYes
Create Location(s)/settings/locationsYes — create manually; Lightspeed does not auto-import locations

After connecting, map each Upvendo location to a K-Series business location in the Locations tab.


FAQs

Q: Does Lightspeed K-Series auto-import my locations? A: No. You create your Upvendo locations manually, then map each one to a Lightspeed business location in the Locations tab.

Q: What Lightspeed products are supported? A: Only Lightspeed Restaurant K-Series is supported. L-Series (a restaurant tier, ex-iKentoo) and R-Series (retail) are not compatible.

Q: Do I need to manage API keys? A: No. The integration uses OAuth 2.0 (Keycloak), so you just log in with your Lightspeed account. Tokens are refreshed automatically.

Q: Does the integration use PKCE? A: No. CSRF protection is provided by an HMAC-signed state parameter (signed with the client_secret). PKCE was used during early development and was removed.

Q: What happens when the access token expires? A: It is refreshed automatically (within a 5-minute buffer, plus a reactive refresh if a call returns 401). If the refresh token itself is revoked, the integration shows a needs_reauth banner and you click Reconnect.

Q: Can I use Lightspeed for customer payments? A: No. Lightspeed handles in-house POS payments only. You must set up Stripe or Viva Wallet for customer-facing payments on Kiosk and Online Ordering.

Q: Can customers redeem a Lightspeed gift card or use Lightspeed loyalty on my Kiosk / Online Ordering? A: No. Lightspeed's API does not let an online order look up a gift-card balance or earn/redeem loyalty points, so these are not connected to Kiosk or Online Ordering. Use Upvendo's built-in gift cards and loyalty for customer-facing ordering — they work independently of the POS. Your Lightspeed gift cards and loyalty continue to work as normal on the Lightspeed POS itself.

Q: Do my Lightspeed modifiers and production instructions sync to Kiosk / Online Ordering? A: Yes — both, automatically on the next menu sync. Lightspeed has two kinds:

  • Modifiers / sub-items (priced add-ons, e.g. a €0.50 sauce) → imported as Upvendo modifier groups; the customer's selection is sent to the POS as a priced sub-item line and prints on the receipt.
  • Production instructions (free prep prompts, e.g. Sauce → Tartar/Ketchup) → imported as free Upvendo modifier groups (single- or multiple-choice, matching how you set them up); the customer's selection is sent to the POS as a production instruction, so it appears on the kitchen/prep ticket (production instructions are kitchen-facing and do not print on the customer receipt — that's normal).

Note: in Lightspeed, an item can have either product modifiers or production instructions, not both — the Lightspeed item editor enforces this (remove the modifiers to add production instructions, or vice-versa). Set both up in the Lightspeed back office; Upvendo picks up whichever an item carries.

Q: My location shows an amber "Degraded" badge after a sync — is my menu broken? A: No. "Degraded" means the menu did sync (items are up to date), but Upvendo could not get a complete picture of your modifier data from K-Series this run, so it deliberately left your modifier groups, modifiers, and production-instruction groups untouched rather than refreshing or deleting them. Your existing modifiers keep working on Kiosk and Online Ordering — they are simply not refreshed from the last clean sync. Click Sync now on that location's row to retry. See Troubleshooting for details.

Q: How do I check if my connection is working? A: Go to /lightspeed → Status tab and click Test Connection.

Q: Is the menu sync one-way or two-way? A: Catalog/menu data is imported one-way (Lightspeed → Upvendo). Orders are the only thing Upvendo pushes back to Lightspeed. Lightspeed is the source of truth — items removed there are deactivated in Upvendo.

Q: How often does the menu sync run? A: Immediately on mapping creation and whenever you click manual sync, plus a scheduled catalog/menu sync that runs once daily at 07:01 by default — your local time, not UTC (your merchant admin can change it via the Auto Sync Time field, settings.sync_time), dispatching one sync job per active mapping. The in-house auto-sync command is scheduled every 30 minutes, but each tick only checks which mappings are due — it is not a 30-minute sync. Order pushes, by contrast, are real-time.

Q: How does the integration handle multiple business locations? A: One OAuth connection covers the account, and you map each Upvendo location to one Lightspeed business location. Menu sync and orders run per mapping; each API call targets the mapped businessLocationId.

Q: What order profiles (account profiles) does Upvendo use, and how do they affect VAT? A: Upvendo tags each pushed order with a K-Series order profile, mapped from the dining option. By default these are the built-in profiles (dine-in → dinein, take-away/delivery → takeaway), which already bill the correct VAT out of the box — but the built-in takeaway profile completes orders the moment they are paid, so prepaid online/kiosk orders skip the POS Orders queue. The recommended setup is to create dedicated order profiles (example codes upvdinein / upvtakeout / upvdelivery — the code is a naming convention only; Upvendo does not require or validate a specific value) with Order completion = "Complete manually in POS" (completionMode = MANUALLY) and the matching deliveryMode (TAKE_AWAY / DELIVERY), then map them per order type in the Order profiles dialog — and have the reduced-rate VAT rules attached to those profiles (in the EU that is a one-time request to Lightspeed support; the onboarding guide has the exact steps and an email template). The order profile is also what drives the VAT rate K-Series applies — e.g. in Belgium, food is taxed 12% for dine-in but 6% for take-away/delivery. See Order profiles & VAT.

Q: Can I view financial reports through the integration? A: The integration has read access to Lightspeed's financial API (tax rates, payment methods, accounting groups, daily financials, sales). A subset is exposed in the backoffice for diagnostics.

Q: How do I disconnect? A: Disconnecting is disabled in production — the Delete button is greyed out and the endpoint returns "contact administrator". To remove a live integration, contact an administrator.


Troubleshooting

"OAuth connection failed"

  • Verify the Lightspeed account is Restaurant K-Series (not L-Series or R-Series).
  • Ensure the account has sufficient permissions to authorize the integration.
  • Check that the redirect URI configured in Lightspeed matches /api/oauth/lightspeed/callback exactly.
  • Retry from the Settings tab → Connect with Lightspeed.

"Connection test failing after successful OAuth"

  • The access token may need refreshing — if you see a needs_reauth banner, click Reconnect.
  • Check the Status tab and the integration logs for error details.
  • Verify the Lightspeed account is active.

"Business ID is missing"

  • The /o/op/data/businesses call may have failed after OAuth.
  • Click Reconnect to re-run the full OAuth + business fetch.

"Token refresh failed" / needs_reauth

  • The refresh token may have been revoked (password change, session revocation, or refresh-token expiry).
  • A needs_reauth banner appears — click Reconnect to re-authorize. Existing mappings and the webhook are preserved/healed.
  • If it persists, verify LIGHTSPEED_CLIENT_ID / LIGHTSPEED_CLIENT_SECRET are configured.

"Webhook not receiving events"

  • Confirm the webhook endpoint is registered and linked to the business location (re-saving a mapping, or reconnecting, re-registers and re-links it).
  • Check that the Basic Auth credentials match LIGHTSPEED_WEBHOOK_USERNAME / LIGHTSPEED_WEBHOOK_PASSWORD.
  • Ensure the callback URL is publicly reachable over HTTPS.
  • Remember only order DELIVERED and item SALES_RESTRICTION_UPDATED are explicitly subscribed; other order/payment statuses arrive automatically.
  • Confirm the Upvendo location is mapped to a business location and the mapping is active (not paused).
  • Trigger a manual sync from the Locations tab.
  • Check the Lightspeed account has active menus and items configured.
  • Review the integration logs for API errors.
  • If the location's badge reads Degraded rather than Active, the items did sync but the modifiers did not refresh — see the next entry.

Where you see it: /lightspeedLocations tab, as an amber Degraded badge on the location's row with this message underneath. (Verified: upvendo-backoffice/src/views/lightspeed/mappingBadge.ts lines 19-30; badge colour degraded: 'warning' in LightspeedLocationsTab.vue line 503.)

What it means: the menu did sync — the item count in the message is real and your items are up to date. What did not happen is the modifier refresh. K-Series returned an incomplete picture of your modifier / production-instruction data this run, so Upvendo deliberately left those groups alone: they were not re-imported and, more importantly, not deleted. Existing modifier groups, modifiers, and item→production-instruction links are preserved exactly as the last clean sync left them. This is the intended safety behaviour — the alternative would be K-Series briefly returning nothing and Upvendo purging every modifier group as an "orphan". (Verified: app/Services/BackOffice/LightspeedKSeriesIntegrationService.php lines 4417-4433 and 4452-4457; the skipped purge at lines 5778-5794; preserved production-instruction group + item link at lines 4913-4944.)

What triggers it (any one is enough):

  • The production-instruction dictionary call (/o/op/1/menu/modifiers) failed. (Verified: lines 4126-4133 and 5060-5088.)
  • That dictionary came back missing a production-instruction group one of your items references, or returned that group with no choices in it. (Verified: lines 4913-4944.)
  • A menu's v2 load (/o/op/2/menu/load/{menuId}) returned an empty menu, so the sync re-fetched it via v1 — and v1 carries no modifier groups at all. (Verified: lines 4253-4270.)

What to do:

  1. Nothing is lost, so there is no urgency — ordering keeps working with the modifiers from the last clean sync.
  2. Click Sync now on that location's row. Most causes are transient and the next run clears the badge back to Active.
  3. If it keeps degrading, check that your production-instruction groups in the Lightspeed back office still have choices attached, and review the integration logs.
  4. After three consecutive degraded syncs on the same location, Upvendo automatically alerts its operations team; a clean success resets that counter. If the badge will not clear, contact support. (Verified: lines 7209-7234, threshold V1_FALLBACK_ALERT_THRESHOLD = 3 at line 43.)

Not the same as: a message reading "Menu sync degraded: every menu returned empty (V2 and V1); orphan purge skipped, catalog left untouched" with a red Error badge. That one means no items synced at all — every menu fetch came back empty — and nothing was purged. Retry the sync and check that the Lightspeed business location still has a published menu. (Verified: lines 4367-4390 — that path sets the status to error, not degraded.)

Item availability returning unexpected results

  • Availability is per-SKU and per-business-location.
  • RESTRICTED (with a count) writes stock; NOT_RESTRICTED is skipped, so stock you expect to clear may simply not be touched by that event.
  • A full menu sync also refreshes availability as a safety net.

Order creation returns an error

  • Ensure the order's location is mapped to a valid, active business location.
  • If mapping itself failed with an error, confirm the Lightspeed business location exists and is active and that your OAuth grant covers it. Tax-exclusive (US / Canada) locations are fully supported and need no special enablement — the tax method is imported automatically from the K-Series taxIncluded flag.
  • Confirm the item SKUs in the order exist in Lightspeed's catalog (unmapped items are skipped).
  • The order is tagged with one of K-Series' built-in order profiles (dinein / takeaway) — no custom profile (e.g. upvtakeout) is required or validated by Upvendo. A profile-related rejection means the location's online-ordering setup doesn't recognise the built-in profile. If you do use dedicated profiles, the mapped takeout/delivery profile must have deliveryMode = TAKE_AWAY / DELIVERY and completionMode = MANUALLY, plus the reduced-rate VAT rule attached.
  • A noAccountingGroup error means an item is missing an accounting group in Lightspeed.
  • Overpayment errors usually indicate a price mismatch with the K-Series catalog. For tax-inclusive (EU) orders, paymentAmount is intentionally the net items subtotal (subtotal − discount) + tip, which matches the total K-Series recomputes from the pushed SKUs. For tax-exclusive (US / Canada) orders, paymentAmount is derived from the K-Series net taxed total (getTotalMoney(), i.e. items + on-top tax, minus gift cards) so the tax K-Series adds is already accounted for. In both cases the tip rides separately as tipAmount.

Disconnecting Lightspeed K-Series

Disconnecting is disabled in production:

  • The backoffice Delete button is greyed out, with a "disconnect disabled" tooltip.
  • The disable endpoint returns "Disconnecting Lightspeed K-Series integration is disabled. Please contact administrator." outside test environments.

To remove a live integration, contact an administrator. When a disconnect does run (test environments / admin), it:

  • Refuses while a sync is in progress.
  • Wipes all K-Series-imported data for every mapped location (items, categories, modifier groups, modifiers, K-Series-sourced tax rates).
  • Best-effort deletes the merchant webhook endpoint on Lightspeed.
  • Hard-deletes the integration record and clears the merchant's provider_integrated marker.

To stop data for a single location without disconnecting, remove that location's mapping in the Locations tab (this wipes the K-Series-sourced data on that location and unlinks its webhook).