Appearance
Lightspeed Restaurant K-Series Integration — Engineering Reference
This is the engineering reference for Lightspeed Restaurant K-Series — the POS we integrate with via OAuth2 (Keycloak) orders-api / financial-api / items / offline_access. Written for backend developers building or maintaining the order-push integration, and for AI agents fixing bugs in it.
Scope of this doc — read first. This page documents how K-Series the product behaves — the externally-observed, empirically-verified knowledge that is not derivable from our code, with citations to the Lightspeed OpenAPI source, the official online-ordering tutorial, and live POS observation. Its centrepiece is the order lifecycle & payment model, because that is where our current implementation diverges from what Lightspeed expects.
For merchant-facing behaviour (OAuth, mapping, menu sync, VAT/order-profiles, FAQ) see
shared/integrations/lightspeed.md. For the certification status (code-adjacent, file:line) seeupvendo-backend/docs/lightspeed-k-series-certification-pack.mdandupvendo-backend/docs/LIGHTSPEED_K_SERIES_INTEGRATION.md.
TL;DR
| Topic | Reality |
|---|---|
| Order routing on the POS | A toGo/local order that is FULLY PAID at create auto-closes to the Receipts screen. An UNPAID order appears in the POS order-management / production queue ("Pickup/Delivery" for to-go, "Dine-in" for local) for the kitchen to prepare. Payment state — not dining mode — decides which. |
payment on order-create | Optional. The official tutorial creates the order without it. Including it (with the full amount) is what triggers the immediate auto-close. |
| Settling payment separately | POST /o/op/1/pay ("Apply a Payment", op apeMakePayment) records a payment against an existing open order by account id. It is asynchronous (returns {"status":"ok"}; the real outcome arrives via the Payment SUCCESS/FAILURE webhook). The OpenAPI documents it only as "add a payment to an existing order" — it does not documentedly close the order; whether full payment auto-closes depends on the profile's completionMode (§1.6). |
| Webhooks | Rich lifecycle: READY_FOR_PICKUP, DELIVERED, Account: CLOSED, CHECK_WAS_UPDATED, CANCELLED, FAILURE, payment SUCCESS/FAILURE, ONLINE_ORDER_READINESS(READY/NOT_READY). |
Tender (paymentMethod) | A merchant-configured payment-method code. OOPAYMENT is the example value in the online-order API (not a documented "standard"); this test merchant records sales against an "Other payment method" coded upvendo. |
| Tip contract | Differs by call: create-order paymentAmount INCLUDES tip; Apply-a-Payment paymentAmount EXCLUDES tip (tip rides as tipAmount). |
| "Prepaid" flag for orders | ❌ None. The only prepaid/deposit mechanism in the API is for Reservations (deposits applied to a reservation's order), not online orders. |
| Current Upvendo behaviour | On the default path we embed full payment in the order-create, so the order is born fully-paid → auto-closes to Receipts → never enters production. Two paths push UNPAID instead: (a) deferred methods (CashOnDelivery/Invoice) always omit payment; (b) the create-unpaid + automated settlement flow behind the default-OFF settings.create_unpaid_settlement flag. See §2. |
1. The order lifecycle & payment model
This is the most important section. Everything below is verified against the Lightspeed OpenAPI source, the online-ordering tutorial, and live observation on a trial POS (2026-06-28).
1.1 Paid → Receipts; unpaid → production queue
The online-ordering tutorial states it plainly:
"Both Local and To Go orders will appear on the receipt screen in the POS when they have been fully paid. Orders awaiting payment appear in [...] 'Dine In' for local orders and 'Pickup/Delivery' for to-go orders."
So the order's payment state decides where it lands on the POS:
- Fully paid at create → the order auto-closes to a Receipt. It never appears in the live order-management/production view; the kitchen/KDS does not get it as a working ticket.
- Not fully paid → the order is open and appears in the order-management queue (
Pickup/Deliveryfor to-go,Dine-infor local), where the kitchen prepares it.
This is dining-mode-independent — a paid dine-in order and a paid take-out order both go straight to Receipts.
1.2 The webhook event model
From the OpenAPI apeEventResource schema (the order-and-pay webhook), the events are:
Upvendo subscribes to ONLY these two:
order→DELIVEREDitem→SALES_RESTRICTION_UPDATED(86 / availability; the webhook's business locations must be linked)
Always sent automatically (auto-delivered — NOT subscribed):
order_api→ONLINE_ORDER_READINESS(READY/NOT_READY)Account→CLOSEDandCHECK_WAS_UPDATEDOrder→READY_FOR_PICKUP,FAILURE,CANCELLEDPayment→SUCCESSandFAILURE
The integrator's thirdPartyReference is echoed back in the webhook payload. The account id is on every webhook as the top-level ikentooAccountIdentifier — that is the id passed to /o/op/1/pay. provideAccount: true (which we set) additionally embeds a full account snapshot (paidAmount, currentAmount, paymentEntries) — but only on the ORDER webhook; the PAYMENT webhook carries no account object (just ikentooAccountIdentifier, plus error.account on failure). Our handler keys on the payload's status/event, so it already receives all of these.
Answer to "is there a webhook telling us an order closed on the POS?" → Yes:
Account: CLOSED(always sent), plusREADY_FOR_PICKUP(kitchen finished) andDELIVERED(handed over).
1.3 Two ways to attach payment
A. Embedded in the order-create (Create To Go / Local Order)
text
payment:
paymentMethod: <merchant tender code> # e.g. OOPAYMENT
paymentAmount: <net subtotal + TIP> # INCLUDES tip
tipAmount: <tip>Including this with the full amount marks the order fully paid → immediate auto-close to Receipts (§1.1).
B. Separate, after create — POST /o/op/1/pay ("Apply a Payment", op apeMakePayment)
text
# required:
businessLocationId, endpointId, paymentAmount, thirdPartyPaymentReference
# optional:
iKaccountIdentifier "A1234.56" # account id — webhook top-level ikentooAccountIdentifier, or open-checks ikaccountId
paymentMethod <merchant tender code>
paymentAmount <net subtotal> # EXCLUDES tip
tipAmount <tip> # recorded as the payment entry's `tip`
staffId, deviceId, taskTtlInMs, targetPrinterProfileIdThe OpenAPI documents this only as "add a payment to an existing order" — it does not documentedly close the order. Live: the call is asynchronous (returns {"status":"ok"}; the result — including ACCOUNT_ALREADY_CLOSED when re-paying an already-closed order — arrives via the Payment webhook, not the response). Whether full payment auto-closes is the profile's completionMode (§1.6): IMMEDIATE → closes to a receipt; MANUALLY → stays open + paid in production. Note the tip contract flips between A and B: create includes tip in paymentAmount; pay excludes it (tip rides as tipAmount).
Open orders can be retrieved via Get All Open Checks (current day), Get Open Check by Table, or Get Single Check by accountId — useful as a reconcile/poll path alongside the webhooks.
1.4 The tender (paymentMethod)
paymentMethod is "the payment method code as configured by the merchant." OOPAYMENT appears as the example value throughout the online-order API — treat it as an example, not a documented standard. The code must exist as a payment method in the merchant's back office (Payment → Payment methods). Merchants commonly create an "Other payment method" for the integrator — e.g. this test merchant uses one coded upvendo (every pushed sale recorded its payment against it). The same tender is used whether payment is embedded (A) or applied separately (B).
1.5 No "prepaid" flag for orders
There is no mechanism to mark an online order as already-paid without closing it. A search of the OpenAPI surfaces prepaid/deposits only under Reservations for Platforms ("prepaid deposits made through the external platform", applied to the reservation's linked order) — not under toGo/local orders. So for a prepaid online order the only paths are: embed payment (closes immediately) or create-unpaid + apply-payment-later (the intended pattern, §3).
1.6 The account profile is the routing lever (deliveryMode + completionMode)
Live-validated 2026-06-28 (via the dev order-push routes + the iPad POS). The accountProfileCode drives three behaviours at once, and they must all be satisfied by the same profile:
| Driver | Field on the profile | Effect |
|---|---|---|
| POS production tab | deliveryMode | NONE → Dine-in tab · TAKE_AWAY / DELIVERY → Pickup/Delivery tab |
| Close-on-full-payment | completionMode | IMMEDIATE → closes to Receipts the instant fully paid · MANUALLY → stays OPEN + paid in production until staff complete |
| VAT | tax-rule "Order profile" condition | which rate K-Series applies (see Order profiles & VAT) |
The order's own fields do not affect the tab — takeAway: true and deliveryAddress were both tested and had zero effect; the tab is set only by the profile's deliveryMode. So for the create-unpaid + settle flow a takeout/delivery order needs a profile that is deliveryMode TAKE_AWAY/DELIVERY (correct tab) and completionMode MANUALLY (stays in production after we record payment) and carries the reduced VAT. On the test merchant:
| Profile | deliveryMode | completionMode | Food VAT | verdict |
|---|---|---|---|---|
dinein | NONE → Dine-in | MANUALLY | 12% | ✓ correct for dine-in |
takeaway | NONE → Dine-in | IMMEDIATE | 6% | right VAT, wrong tab + closes-on-pay |
upvtakeout | TAKE_AWAY → Pickup/Delivery | MANUALLY | 6% | ✓ correct (Lightspeed-mapped 2026-06-29) |
upvdelivery | DELIVERY → Pickup/Delivery | MANUALLY | 6% | ✓ correct (Lightspeed-mapped 2026-06-29) |
RESOLVED (2026-06-29). The reduced VAT is set by a tax rule keyed on the order's Account profile — each tax rule pairs a Tax rate (BTW6 / BTW12 / BTW21) with an Account profile (All / Take away / Upvendo Takeout / …) on a given tax profile (Voedsel = food, Dranken = non-alcoholic drink, Alcohol). So the 6% is not bound to the literal takeaway code; it follows whatever Account profile the tax rule names. K-Series gates tax-rate / tax-profile configuration to US & Canada only — "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." (Managing tax settings) — so an EU merchant cannot self-edit tax rules, but Lightspeed applies them on request (the standard EU partner process). For Le Friet Délice, Lightspeed (partner contact Ali Masoumie, Partnerships) added BTW6 / Account profile = Upvendo Takeout and = Upvendo Delivery rules on Voedsel + Dranken, so the custom codes upvtakeout / upvdelivery now bill the reduced rate (6% food + non-alcoholic drink, 21% alcohol — alcohol has no reduced rule), verified via the tax-breakdown oracle + a real order. The create-unpaid flow on these profiles therefore now has correct VAT + correct Pickup/Delivery tab (deliveryMode TAKE_AWAY/DELIVERY) + completionMode MANUALLY all at once. Historical fork for context: native takeaway already had 6% but deliveryMode NONE + completionMode IMMEDIATE; the custom profiles had the right tab + completion but needed the BTW6 rule mapped onto them — now done.
Design fork (historical — now resolved 2026-06-29): native takeaway = 6% VAT but deliveryMode NONE + completionMode IMMEDIATE (wrong tab + auto-closes on pay → breaks create-unpaid); custom upvtakeout/upvdelivery = correct tab + MANUALLY (stays open for deferred settle) and now also 6% VAT since Lightspeed mapped them into the BTW6 Account-profile rule. The shipped default (PR #1064) routes takeout/delivery → takeaway; a merchant can override to the custom profiles via settings.account_profiles to get the correct flow and the reduced VAT together — no longer a VAT-vs-flow trade-off. Because the create-unpaid profiles are MANUALLY, settling does not close the order — /o/op/1/pay can run as soon as we have the account id; the order stays paid + in production until staff complete it.
Account-id field gotcha: the open-checks LIST returns the account id as
ikaccountId(e.g.A140704.24); the webhook payload uses top-levelikentooAccountIdentifier. The/o/op/1/paybody wantsiKaccountIdentifier. Read the right field per source.
2. Current Upvendo behaviour — and the gap
What we do (LightspeedKSeriesIntegrationService::buildOrderPayload): we capture the customer's payment on our side (Stripe), then push the toGo/local order with the payment block embedded — paymentAmount = net subtotal (− discount) + tip, on the create call. Per §1.1, that makes every order fully paid at create.
The consequence: every online and kiosk order auto-closes to Receipts and never enters production.
Evidence (live, 2026-06-28, 5 pushed orders R140704.17–.21, pickup + delivery + dine-in):
financial/daily→ all five present as closed receipts.Get Open Checks→ empty[](nothing open).- POS Order management → Pickup/Delivery (0), Dine-in (0) — "No orders to display."
Why it matters:
- The kitchen never sees online/kiosk orders in the production queue / KDS — they only ever appear as paid receipts (and only print a kitchen docket if a printer/KDS is configured for the course).
- We never benefit from the production-lifecycle webhooks (
READY_FOR_PICKUP,DELIVERED,Account: CLOSED) — the order is "done" the instant it's created. - Order notes are collateral damage: the K-Series
orderNoteprints on the kitchen docket / shows in production — which our orders never reach. - Scheduled take-out/delivery is the sharpest case: a scheduled prepaid order should sit in the queue and fire at its time; auto-closing it to a receipt risks it never reaching the KDS. (A now-fixed bug compounded this:
buildOrderPayloadsentscheduledTimeForOrderAsIso8601— a local-order field — on every scheduled order, which K-Series 500s on the toGo endpoint; scheduled take-out/delivery never reached the POS. The fix gates the timestamp per endpoint:orderCollectionTimeAsIso8601for toGo,scheduledTimeForOrderAsIso8601for local.)
We have a /o/op/1/pay wrapper (applyPayment()). With the default-OFF settings.create_unpaid_settlement flag off, it is dev-endpoint only — the default (embedded-payment) push never calls it. With the flag on (prepaid, non-dine-in), the automated flow calls it: the order is pushed unpaid with a settlement snapshot → the fulfilment webhook dispatches the settle job → settleLightspeedOrder() → applyPayment() (/o/op/1/pay) fires on the configured settle trigger (settings.settle_trigger, default READY_FOR_PICKUP). A stuck-order sweep is a second automated caller.
3. Target flow (the design we're moving to)
Status (2026-06-28): this flow is implemented behind a default-OFF flag (
settings.create_unpaid_settlement) and live-validated end-to-end — create-unpaid → correct production tab/type (Pickup / Delivery, shown "In preparation" on the KDS) →/o/op/1/paysettle → VAT preserved on the receipt; tip correct in both directions; idempotent on duplicate reference. The Lightspeed-side VAT prerequisite (§1.6) is now RESOLVED (2026-06-29): Lightspeed mapped the customupvtakeout/upvdeliveryprofiles into the BTW6 Account-profile tax rule (Voedsel + Dranken), so create-unpaid on those profiles now bills the correct reduced VAT (6% food + non-alcoholic drink, 21% alcohol) and lands in the right Pickup/Delivery tab — no longer a 6%-vs-flow trade-off. Scope: create-unpaid is to-go only — dine-in is deliberately out of scope, since alocalorder auto-closes on full payment whatever thecompletionMode.
The Lightspeed-intended flow for a prepaid online order, reconciled from the tutorial (create body has no payment), the API ref (payment optional), and the webhook model:
- Capture payment on our side (Stripe) — unchanged. We hold the money.
- Create the
toGoorder WITHOUT thepaymentblock → the order is unpaid. Dine-in (local) orders are excluded: they auto-close on full payment regardless ofcompletionMode, so they always keep the embedded payment block (shouldCreateUnpaid()returns false for any dine-in transaction —LightspeedKSeriesIntegrationService.php:3805-3807, with the fallback logged once at the decision site inbuildOrderPayload:3575-3582). - K-Series puts it in the order-management / production queue (
Pickup/Delivery) → kitchen sees it, KDS ticket +orderNoteprint. - We receive the create/confirmation webhooks including the account id (
provideAccount: true). - We track the lifecycle via webhooks:
READY_FOR_PICKUP→DELIVERED→Account: CLOSED(and/orONLINE_ORDER_READINESSREADY). - To record the already-collected money in K-Series, call
POST /o/op/1/paywith the account id + the merchant tender,paymentAmountexcluding tip + separatetipAmount(§1.3.B) → records payment and closes out the order.
Settle trigger (step 6) — RESOLVED & IMPLEMENTED. The settle trigger is per-merchant configurable via settings.settle_trigger, default READY_FOR_PICKUP; the only alternative is CHECK_WAS_UPDATED (self-trigger-guarded to avoid re-firing on our own payment write). DELIVERED is a tracked lifecycle status, not a selectable settle trigger, and there is no "leave the order open until the merchant closes it on the POS" mode. Between create and settle the order legitimately shows as unpaid in Pickup/Delivery — per the tutorial that is where awaiting-payment orders live, so it is the accepted interim state; staff must simply not re-collect. There is no "paid-but-keep-open" shortcut (§1.5).
Implementation deltas (for the adjustment work):
- Stop embedding the
paymentblock inbuildOrderPayloadfor the online-order path; create unpaid. - SHIPPED:
applyPayment()(/o/op/1/pay) is now wired into the automated settle flow (settleLightspeedOrder()), fed by the account id from the webhook payload and triggered onsettings.settle_trigger(defaultREADY_FOR_PICKUP). - Re-base the
paymentAmounttip handling for the pay call (exclude tip; passtipAmount). - Act on the inbound
READY_FOR_PICKUP/DELIVERED/Account: CLOSEDwebhooks to drive settlement + Upvendo order-status. (/o/op/1/payis async — confirm settlement via thePayment SUCCESSwebhook, not the call's response.) - Scheduled pre-orders: send the per-endpoint timestamp only —
orderCollectionTimeAsIso8601for toGo (Pickup/Takeout/Delivery),scheduledTimeForOrderAsIso8601for local (Dine-in). Sending the local field on a toGo create 500s (live-confirmed).
4. EU tax-rule onboarding SOP (per merchant — internal)
Every EU merchant that adopts the dedicated upv* order profiles needs a one-time Lightspeed-side tax-rule setup — EU merchants cannot edit tax rules in the Back Office (US/CA-only UI), so this is the single external dependency in Lightspeed onboarding. The merchant-facing steps + the Lightspeed email template live in merchant/onboarding/lightspeed.md → Order profiles & VAT setup. Internal procedure:
- Profiles first — merchant creates
upvdinein/upvtakeout/upvdelivery(Takeaway mode Off / Pick up / Delivery; Order completion "Complete manually in POS"), then maps them via Lightspeed → Locations → ⋯ → Order profiles (persists to the mapping'ssettings.account_profiles;resolveOrderProfileCodehonours it). - Tax-rule request — merchant (or we, CC'ing the account owner) emails K-Series support with business ID, business-location ID, the profile codes, and the accountant-confirmed target rates. Ask = "attach the reduced take-away rules to
upvtakeout/upvdelivery, mirroring the built-in Take away profile." - Go-live gate — verify before enabling ordering channels: call the per-profile tax preview,
GET /api/back-office/lightspeed/account-profiles/tax-preview(routeroutes/api/backoffice/lightspeed.php:53; the account-profiles list is:50; the controller actionpreviewAccountProfileTaxeswraps the service methodpreviewProfileTaxRates→ the/tp/v1/business-locations/{blId}/tax-breakdownoracle), and check each profile returns the expected rates (BE:upvtakeout/upvdelivery→ 6% food + non-alc drink / 21% alcohol;upvdinein→ 12/21/21). Then one real take-away test order; confirm receipt VAT + that it lands on the POS Orders screen (Pickup tab). - Do not point takeout/delivery back at the built-in
takeawayprofile to "fix" VAT — correct rate, butcompletionMode IMMEDIATEauto-completes paid orders off the Orders screen.
Product follow-up (backlog): surface the tax preview per profile inside the backoffice Order-profiles dialog (backend endpoint is live; the dialog does not call it yet) + generate the Lightspeed email pre-filled with the merchant's IDs.
5. Modifiers, sub-items & production instructions
K-Series has TWO distinct customization concepts; an item can have either, never both (the Back-Office item editor enforces this — "remove all modifiers to activate production instructions"). Both sync into Upvendo modifier groups and both push on orders.
- Product modifiers / sub-items (PRICED, e.g. a €0.50 sauce). Source: the V2 menu item's
menuModifierGroups[].productModifiers[]→syncModifierGroupsFromMenu. Each choice is a real catalog product (sku).external_ids.lightspeed=<groupSku>(group) /<groupSku>_<modifierSku>(choice). On an order the chosen modifier is pushed as a subItem (items[].subItems[]withcustomItemPrice) — a separate priced line that prints on the customer receipt. - Production instructions (FREE prep prompts, e.g. Sauce → Tartar/Ketchup). The API confusingly calls THESE "modifiers". →
syncProductionInstructionGroupsFromMenuinto a NoCharge modifier group (single/multi frommultiSelectionPermitted). ⚠️ Two-source shape: the V2 menu item'sproductionInstructionList[]carries only the group ref (id/name/multiSelect) — the CHOICES (instruction+ikentooModifierId) live ONLY ingetMenuModifiers(/o/op/1/menu/modifiers), fetched once per business location as the choice dictionary (collectProductionInstructionDictionary).external_ids.lightspeed=pi:<productionIntructionGroupId>(group) /pi:<groupId>_<ikentooModifierId>(choice) — thepi:prefix namespaces the int64 ids AND letsbuildOrderLineItemsdetect them. On an order the chosen instruction is pushed asitems[].modifiers[].modifierId(= the ikentooModifierId), K-Series' native field. It appears on the kitchen/prep ticket, NOT the customer receipt or financial reports (production instructions are kitchen-facing, carry no price, and are absent from reports).
Both share the same reconcile + orphan-purge (merged into $modifierGroupIdMap / $seenModifierIds). The tax-breakdown oracle (recordTaxBreakdown → buildTaxBreakdownRequest) is sent base items only — customItemPrice, production-instruction modifiers, AND sub-items are stripped, because the oracle compares base-item gross and 422s ("Issue with tax profile set up") on a sub-item whose modifier product lacks a K-Series accounting group (order-create tolerates it; the oracle doesn't).
Code: LightspeedKSeriesDataMapper::mapProductionInstructionGroup/mapProductionInstructionModifier; LightspeedKSeriesIntegrationService::{syncProductionInstructionGroupsFromMenu, collectProductionInstructionGroups, collectProductionInstructionDictionary}; buildOrderLineItems (the modifiers[] branch); buildTaxBreakdownRequest. PRs #1223 (import + push), #1227 (choices from getMenuModifiers), #1228 (base-items-only oracle).
6. Capability boundaries — gift cards, loyalty, discounts
Verified against the K-Series OpenAPI (partner API) + Lightspeed's gift-card integration guide (api-portal.lsk.lightspeed.app/guides/integration-guides/gift-cards). These are deliberately not wired — the API doesn't support them for a prepaid online/kiosk flow, so this is a platform boundary, not a backlog gap.
- Gift cards — not viable for prepaid online/kiosk. The integration has zero gift-card handling, and
buildOrderPayloadexplicitly excludes gift-card purchases from the pushed items. Lightspeed's guide states: "creating and/or adding funds to gift cards via API is not supported. Only redemption is currently supported." Both redemption paths miss our model:POST /o/op/1/pay(apeMakePayment) applies a payment with a merchant-preconfigured gift-card payment method, but the body (apeStandalonePayment) carries onlypaymentMethod+paymentAmount+ order ref — no gift-card-number field, and there is no balance-check endpoint anywhere in the API. It records a gift-card tender against an order; it cannot look up or decrement a specific customer's card.- Web Extension JS (
pos_payAccount/pos_addPaymentfor partials) manages the balance interactively, but only when the integration runs as a Lightspeed Web Extension on the POS device — not Upvendo's standalone API integration. - Our flow is prepaid (charge Stripe, then push a fully-paid order); with no balance lookup we cannot split gift-card vs Stripe before charging.
/id-cards/v1/.../batches(id-cards-apiCreateIdCardBatch) is issuance scaffolding, and the guide confirms issuing/top-up is not API-supported. → Use Upvendo-native gift cards for online/kiosk; do not advertise "Lightspeed gift cards online."
- Loyalty — no online earn/redeem. Loyalty schemas exist (
backendv3LoyaltyProgramDto,PunchLineDto) but there is no/loyalty/endpoint — nothing to earn or redeem against online. Use Upvendo-native loyalty. - Discounts — partly wired, but the write half clashes with our model. K-Series exposes
GET /o/op/1/menu/discounts(list) + adiscountCodeon order lines. We do call the list endpoint — it is wired end to end (app/Services/BackOffice/LightspeedKSeries/LightspeedKSeriesConstants.php:128→app/Services/Common/LightspeedKSeriesApiClient.php:447→app/Services/BackOffice/LightspeedKSeriesIntegrationService.php::getMenuDiscounts():1336→GET /back-office/lightspeed/menu-discounts,routes/api/backoffice/lightspeed.php:76) and exposed read-only. We never senddiscountCodeon an order line (zero occurrences in the backend): Upvendo owns online-ordering pricing and pushes the netcustomItemPriceso K-Series books the discounted total (see §2). Applying a K-SeriesdiscountCodeon top would double-count vs the Stripe-prepaid amount. Only revisit if a merchant needs K-Series-configured discounts reflected verbatim online.
Validation impact: none — the K-Series Order & Payment onboarding pack has no gift-card / loyalty scope; just don't claim them in the marketplace product description.
7. Related
- VAT / order profiles — K-Series keys VAT on the order profile, per item class (live-confirmed on receipts + the tax-breakdown oracle, BE rates): food 12% dine-in / 6% takeaway; non-alcoholic drink 21% dine-in / 6% takeaway; alcohol 21% always.
takeaway,upvtakeout, andupvdeliveryall carry the reduced rate (Lightspeed added BTW6 Account-profile rules on Voedsel+Dranken, 2026-06-29); configured Lightspeed-side in the EU. Configured in the K-Series back office, not the API. Seeshared/integrations/lightspeed.md→ Order profiles & VAT. - Menu sync (Lightspeed → Upvendo, one-way) and OAuth/mapping — see the merchant-facing doc.
- Certification —
upvendo-backend/docs/lightspeed-k-series-certification-pack.md,upvendo-backend/docs/LIGHTSPEED_K_SERIES_INTEGRATION.md,upvendo-backend/docs/lightspeed-k-series-us-tax-exclusive-scoping.md(US tax-exclusive scoping).
8. References
- Lightspeed OpenAPI source —
apeEventResource(webhook events),Create To Go Order(paymentoptional;paymentAmountincludes tip),Apply a Payment//o/op/1/pay= opapeMakePayment(paymentAmountexcludes tip; documented as "add a payment" — close-on-pay iscompletionMode-dependent, not documented),Reservations(only placeprepaiddeposits exist). - Online Ordering Basics tutorial — create-order body without
payment; "awaiting payment → Pickup/Delivery; fully paid → receipt screen." - Endpoints: Create To Go / Local Order, Apply a Payment, Get All Open Checks, Get Single Check, Order-and-Payment Webhook group.
- Our code —
app/Services/BackOffice/LightspeedKSeriesIntegrationService.php:buildOrderPayload(embedded payment),applyPayment(/o/op/1/paywrapper —:1466; called bysettleLightspeedOrder():1491whensettings.create_unpaid_settlementis on, by the stuck-order sweepSettleStuckLightspeedKSeriesOrders, and exposed atPOST /back-office/lightspeed/pay,routes/api/backoffice/lightspeed.php:94), webhook subscription (subscribeTo: DELIVERED,provideAccount: true).