Appearance
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:
- Upvendo builds an authorization URL with
client_id,response_type=code,redirect_uri, the OAuthscopelist, and an HMAC-signedstateparameter. - The state parameter is an HMAC-SHA256-signed payload (
vendor_id+timestamp) protecting against CSRF. It is signed and verified with the Lightspeedclient_secret. - The merchant is redirected to Lightspeed's Keycloak login and authorizes Upvendo.
- Lightspeed redirects back to the callback (
/api/oauth/lightspeed/callback, also aliased at/api/lightspeed/callback) with an authorizationcodeand thestate. - Upvendo verifies the state HMAC, then exchanges the code for access and refresh tokens. The token exchange sends
client_id:client_secretvia HTTP Basic Auth in the Authorization header (NOT in the POST body), per Keycloak's requirements. - The business is fetched from
/o/op/data/businesses; the first business's ID and name are stored alongside the tokens. - 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
stateparameter.
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 flaggedneeds_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_accessrefresh 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 hardinvalid_grantand a forced re-auth. Integrations already flaggedneeds_reauthare 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_reauthmid-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 Type | Endpoint | Notes |
|---|---|---|
| Businesses | /o/op/data/businesses | Returns businesses, each with businessLocations[]; first business stored on connect |
| Account Profiles | /o/op/data/account-profiles | Order profiles, filterable by businessLocationId / tag code |
| Floor Plans | /o/op/data/{blId}/floorplans | Table layout per business location |
| Menus | /o/op/1/menu/list | Menu 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/modifiers | Production 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/discounts | Available discounts |
| Item Availability | /o/op/1/itemAvailability | Stock availability per SKU per location |
| Orders (local) | /o/op/1/order/local | Create dine-in orders |
| Orders (toGo) | /o/op/1/order/toGo | Create takeaway/delivery orders |
| Open Checks | /o/op/1/order/table/getCheck | Current open orders/checks |
| Payment | /o/op/1/pay | Apply 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/webhook | Create (PUT), update (POST), get/delete (GET/DELETE), link/unlink locations |
| Tax Rates | /f/finance/{blId}/tax-rates | Tax rate codes and multipliers |
| Payment Methods | /f/finance/{blId}/paymentMethods | Available POS payment methods |
| Accounting Groups | /f/finance/{blId}/accountingGroups | Revenue accounting groups |
| Financials | /f/finance/{blId}/financials/{from}/{to} | Sales for a date range |
| Daily Financials | /f/finance/{blId}/dailyFinancials | Sales for the current business day |
| Tax Breakdown | /tp/v1/business-locations/{blId}/tax-breakdown | Computes tax breakdown (exposed via dev endpoint only) |
| Items API | /items/v1/items | List, create, update, delete items |
| Rich Items | /i/richItem/{businessId} | Images, descriptions, allergens per SKU |
| Allergens | /i/allergens | All 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 atableNumber) - ToGo orders (
/o/op/1/order/toGo) — for takeaway and delivery orders
Actions
Create Locations (Must Be Done First)
Route: /settings/locations
- Create your Upvendo location(s) manually.
- Lightspeed does not auto-import locations into Upvendo.
Connect via OAuth
Route: /lightspeed → Settings tab
- Click Connect with Lightspeed.
- You are redirected to the Lightspeed (Keycloak) login.
- Log in with a Lightspeed Restaurant K-Series account with sufficient permissions.
- Authorize Upvendo.
- You are redirected back and land on the Locations tab.
- The integration stores your access token, refresh token, and business ID automatically.
Map Locations
Route: /lightspeed → Locations tab
- The tab lists your Lightspeed business locations and your Upvendo locations.
- Map each Upvendo location to one Lightspeed business location.
- Creating the mapping links the webhook for that business location and starts the initial menu sync.
- 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
taxIncludedflag on each rate (taxIncluded = false→tax_method = Exclusive;true→Inclusive), 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: /lightspeed → Status tab
- Check the Connection Status card.
- Click Test Connection to verify API access (it calls
/o/op/data/businesses). - Recent activity is shown in the integration logs below the status.
Reconnect (Token Revoked / needs_reauth)
Route: /lightspeed → Settings tab (a warning banner also appears when needs_reauth)
- Click Reconnect to re-authorize via OAuth.
- Existing mappings are preserved and the webhook is healed/re-linked automatically.
- A successful re-auth clears the
needs_reauthstatus.
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_reauthand must be reconnected via OAuth. - The OAuth
stateparameter is HMAC-SHA256-signed with theclient_secretto 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.statusis set todegradedwhen 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'slast_sync. (Verified:app/Services/BackOffice/LightspeedKSeriesIntegrationService.phplines 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 = 3at 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:01by default (overridable per merchant viasettings.sync_time), dispatching one sync job per active mapping. That time is a local wall-clock read in the merchant'ssettings.sync_timezone(defaulting toEurope/Brussels), not UTC, so it does not drift across daylight-saving transitions. The in-houseauto-synccommand 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 anexternal_ids.lightspeedmarker. 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/lightspeedand 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
DELIVEREDand itemSALES_RESTRICTION_UPDATEDevents; 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/retriedOrder Payload Structure
Each order pushed to Lightspeed includes (built in syncOrderToLightspeed / buildOrderPayload):
endpointId— the merchant webhook endpoint (upvendo-{merchantId}) for status callbacksbusinessLocationId— 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/DELIVERYandcompletionMode=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 belowmaxTimeToAttemptOrderDeliverToPos— how long K-Series retries delivery to a register before emitting a FAILURE (5 minutes)items[]— each with a resolvedsku,quantity, and modifiers assubItems[](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 ordersorderNote— 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 docketspayment— 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_settlementoption: prepaid, non-deferred, non-dine-in orders are pushed unpaid, then settled via/o/op/1/paywhen the fulfilment webhook fires (default triggerREADY_FOR_PICKUP, viasettings.settle_trigger). With that option off,/o/op/1/payis 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 option | accountProfileCode | Food | Non-alcoholic drink | Alcohol |
|---|---|---|---|---|
| Dine-in (eat in) | dinein | 12% | 21% | 21% |
| Take-away / Delivery / Pickup (online) | takeaway | 6% | 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/takeawayprofiles, which already bill the correct VAT — but the built-intakeawayprofile 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 matchingdeliveryMode(TAKE_AWAYfor takeout,DELIVERYfor 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
FAILUREwith 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
CANCELLEDevent used to re-clearexternal_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
| Behaviour | Detail |
|---|---|
| Proactive refresh | Access token refreshed within a 5-minute buffer of expiry before authenticated calls |
| Reactive 401 retry | On a 401, the client force-refreshes the token once and replays the request |
| Refresh-token expiry | Tracked separately (refresh_token_expires_at); Keycloak rotates the refresh token on each refresh |
| Refresh failure | An invalid_grant from Keycloak flags the integration needs_reauth and surfaces a Reconnect banner |
| Daily keep-alive | A 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 |
| Concurrency | Refreshes 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 / refresh | Uses 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_USERNAMEandLIGHTSPEED_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 inlocal.) - 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 toDELIVERED(resourceorder) andSALES_RESTRICTION_UPDATED(resourceitem). - 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).RESTRICTEDwith an integer count writes stock.NOT_RESTRICTED(countnull) 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 callssyncItemAvailability()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).
- ORDER — branched on
- 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 orderDELIVEREDand itemSALES_RESTRICTION_UPDATEDare 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 viaupdateRichItemPicture - 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:
| Task | Route | Required |
|---|---|---|
| Payment Profile | /settings/payments | Yes (Stripe or Viva Wallet) |
| Billing Profile | /settings/billing | Yes |
| Branding Profile | /settings/brand | Yes |
| Create Location(s) | /settings/locations | Yes — 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/callbackexactly. - 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_reauthbanner, 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/businessescall 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_reauthbanner appears — click Reconnect to re-authorize. Existing mappings and the webhook are preserved/healed. - If it persists, verify
LIGHTSPEED_CLIENT_ID/LIGHTSPEED_CLIENT_SECRETare 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
DELIVEREDand itemSALES_RESTRICTION_UPDATEDare explicitly subscribed; other order/payment statuses arrive automatically.
Menu or item data not appearing
- 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.
"Menu synced (degraded): N items — modifier groups not refreshed this sync"
Where you see it: /lightspeed → Locations 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:
- Nothing is lost, so there is no urgency — ordering keeps working with the modifiers from the last clean sync.
- Click Sync now on that location's row. Most causes are transient and the next run clears the badge back to Active.
- If it keeps degrading, check that your production-instruction groups in the Lightspeed back office still have choices attached, and review the integration logs.
- 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 = 3at 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, notdegraded.)
Item availability returning unexpected results
- Availability is per-SKU and per-business-location.
RESTRICTED(with a count) writes stock;NOT_RESTRICTEDis 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
taxIncludedflag. - 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 havedeliveryMode=TAKE_AWAY/DELIVERYandcompletionMode=MANUALLY, plus the reduced-rate VAT rule attached. - A
noAccountingGrouperror 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,
paymentAmountis 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,paymentAmountis 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 astipAmount.
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_integratedmarker.
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).