Skip to content

Kassanet Integration

Overview

Kassanet is the underlying POS platform used by both Hendrickx and Vanhoutte POS providers. This integration handles the core communication protocol with the Kassanet API, including (optionally encrypted) data exchange, menu import, and order submission. Order submission is a three-stage flow per order: register the items on a POS "guest check" (table), retrieve the resulting bill, and record the payment tender. Hendrickx and Vanhoutte share one implementation: AbstractKassanetService provides the full functionality, and HendrickxService / VanhoutteService extend it only to set the provider value (hendrickx or vanhoutte). There is no provider-specific behaviour beyond that name.

Purpose

This integration lets you connect a location to a Hendrickx or Vanhoutte POS register to import the POS menu into Upvendo and automatically push customer orders to the POS after a customer pays. The order is registered on the POS as a guest check and the payment tender (e.g. Viva Wallet or Stripe) is recorded against that check.

Key Concepts

  • Provider-Based Architecture: Kassanet is not a standalone integration page. It is accessed through either the Hendrickx (/hendrickx) or Vanhoutte (/vanhoutte) integration. The {provider} URL segment (hendrickx or vanhoutte) selects which service handles the request.
  • Blowfish Encryption (default on): API communication uses Blowfish CBC encryption (zero IV, space padding) with a shared encryption token, controlled by the enable_encryption flag which defaults to true. When enabled, the request body is encrypted and a SEC1 path segment is inserted into the endpoint URL; responses are decrypted. There is no toggle for this in the connect form.
  • HMAC Signature Authentication: Every request carries a Signature header — a base64-encoded HMAC-SHA256 of Date + body computed with the integration's signature key — plus Date and request-id headers.
  • Three-Stage Order Push: An order is pushed in stages: create (POST CreateOrder to register items on a table), bill (GET GetBill to read the POS total), pay (POST PayBill to record the tender). Each stage is its own job, so a failure can be requeued from the exact stage that failed.
  • Chained Import Pipeline: Menu import is split into a chain of sequential background jobs to avoid timeouts: ImportKassanetMenuJob -> ImportKassanetCategoriesJob -> ImportKassanetProductsJob -> ImportKassanetDisplayGroupsJob. Each job processes one stage and dispatches the next.
  • Virtual Table Numbers: For order push the integration claims a free POS table/guest-check number via GetFreeTableNumber, verifies it is empty via GetBill, and locks it. These are POS guest-check slots used to register online/kiosk orders; they are not Upvendo in-house "table sections" and no QR codes are generated for them. The list is stored in settings.tables and per-number usage in settings.tables_usage.

Prerequisites

  • An active Hendrickx or Vanhoutte POS register reachable from the Upvendo server, with the Kassanet API enabled.
  • The POS register must have products, categories, and product selections configured (these become Upvendo items, categories, and display groups).
  • A location created in Upvendo (Settings > Locations).
  • A branding profile configured for the merchant — this is enforced at connect time. Payment and billing profiles are optional and can be set up later.
  • The location must not already have another in-house POS channel connected (see Business Rules).
  • For the order tender to be recorded, the POS register must expose the matching payment type (e.g. a Webshop type, and Viva Wallet / Stripe for the relevant channels).

Setup Steps

Step 1: Prepare Prerequisites

  1. Configure branding at Settings > Brand (/settings/brand) — required.
  2. Optionally set up payment providers at Settings > Payments and billing at Settings > Billing.
  3. Create your location at Settings > Locations (/settings/locations).

Step 2: Connect the POS

  1. Go to Hendrickx (/hendrickx) or Vanhoutte (/vanhoutte).
  2. Open the connect form and enter your Kassanet credentials:
    • Connection URL (base_url): the Kassanet API endpoint URL (validated as a URL).
    • Signature Key (signature_key): HMAC-SHA256 signature key.
    • Encryption Token (encryption_token): Blowfish encryption key.
  3. Optionally set a Daily Sync Time (sync_time) for automatic menu sync. The value must be HH:00 or HH:30 (minutes are restricted to 00 or 30). The time you enter is the local time at the location and is stored as that local time, so it does not shift when the clocks change. A caption under the field names the timezone the sync will run in.
  4. Saving the form auto-triggers a menu sync immediately after connecting.

Step 3: Sync Menu

  1. Click Sync Now to trigger a menu import (also runs automatically right after connecting).
  2. The system dispatches ImportKassanetMenuJob, which runs the chained import:
    • Categories and subcategories first (GetAllProducts).
    • Products and modifiers next.
    • Display groups (GetProductSelection), then stock levels (GetCountdownPluState), placeholder cleanup, a menu reload, and a final connection test.
  3. After completion, the menu appears under Menus > Items (/menus/items).

Step 4: Test the Connection

  1. Click Test Connection on the integration page.
  2. The system calls the Kassanet test endpoint and reads IsSuccessStatusCode.
  3. On success it also resolves/caches the Webshop payment type, records last_successful_test, and reads the tip-calculator flag from SystemInfo.
  4. If the location was previously disconnected, a successful test marks it connected, enables a 15-minute "bulk requeue" window, and reprocesses any orders flagged need_requeue.

Data Sync Details

What Syncs

Kassanet EntityUpvendo EntityNotes
Categories / SubCategoriesCategoriesGetAllProducts; parent categories + nested subcategories.
ProductsItemsIncluding price, kitchen name, and external_ids.{provider}.
Product ModifiersModifier Groups + ModifiersModifier groups merged after products.
Product SelectionsDisplay GroupsGetProductSelection; selections map to display groups, items batch-assigned.
Countdown PLUsInventoryGetCountdownPluState stock levels synced after display groups.
Table Numberssettings.tablesTableNumbers; virtual POS guest-check slots for order push (not in-house table sections, no QR codes).

Sync Direction

  • Menu Import (Kassanet -> Upvendo): One-way. Categories, products, modifiers, display groups, stock levels, and table numbers are pulled from the Kassanet API.
  • Order Push (Upvendo -> Kassanet): One-way, three stages — CreateOrder (register items) -> GetBill (read total) -> PayBill (record tender). The create stage runs in createOrder() (dispatched via ProcessOrderJob); bill and pay run in GetKassanetBillJob and PayKassanetBillJob, both on the payments-medium queue.
  • Auto-Sync: If sync_time is set, a scheduler runs a full menu import daily at that time. sync_time is stored as a local wall-clock time (not UTC) together with the resolved IANA zone in sync_timezone; the scheduler converts the pair to the next absolute instant and manages next_run_time. Because the stored value is local, the sync keeps firing at the same local time across daylight-saving transitions.

Conflict Resolution

  • During import, existing records are matched by external_ids.{provider} (items/modifiers/modifier groups) and external_data.{provider} (categories/display groups). Matches are updated; new entities are created.
  • Modifier group/item ID links are reset before product import (resetKassanetIds) so the mapping is rebuilt cleanly each run.
  • Placeholder items are removed after import: items/modifiers whose kitchen_name is zz, or matches a * pager * pattern (case-insensitive), or whose kitchen name is a numeric value equal to the default name.

Actions

All routes are under /api/back-office/kassanet/{provider}/{locationId} (note the hyphen in back-office), with {provider} being hendrickx or vanhoutte.

Connect

  • Initiate / update: POST /api/back-office/kassanet/{provider}/{locationId} — saves credentials (base_url, signature_key, encryption_token) and sync_time (plus an optional sync_timezone, which the back-office does not send). Re-posting on an existing integration updates it in place (no duplicate). Connecting auto-triggers a menu sync. There is no separate auto-sync-schedule endpoint; the sync time travels with this same save.

Sync / Status

  • Sync Menu: POST /api/back-office/kassanet/{provider}/{locationId}/sync-menu — runs the chained import.
  • Test Connection: GET /api/back-office/kassanet/{provider}/{locationId}/test — tests connectivity, updates connection status, and (on success) requeues stuck orders.
  • Get Status: GET /api/back-office/kassanet/{provider}/{locationId}/ — returns credentials, sync status/logs/stats, tables, the sync time verbatim (it is already a local wall-clock, so no conversion is applied) plus its sync_timezone, and payment_method_status banner flags.

Disconnect

  • Delete is disabled. DELETE /api/back-office/kassanet/{provider}/{locationId} always returns HTTP 400 with "Disconnecting {Provider} integration is disabled. Please contact administrator." The underlying cleanup routine (which would remove categories, display groups, items, menus, modifiers, inventory linked to the provider and fire a ReloadMenu event) exists in code but is not reachable from the API.

Order Flow

  • createOrder() (create stage): After payment is confirmed, assigns a free table number, reconciles against any prior attempt to avoid duplicate registration, then POSTs CreateOrder with the line items, an order-type PLU marker (for KDS), and order info text. On success it dispatches GetKassanetBillJob. Deferred payment methods (Cash on Delivery / Invoice) skip the bill+pay stages — the order stays open/unpaid on the POS.
  • GetKassanetBillJob (bill stage): Calls saveKassanetBill() which reads GetBill, compares the POS bill total to the transaction total (logs a mismatch; a €0 bill against a non-zero transaction marks the order unrecoverable — the table was likely cleared on the POS), then dispatches PayKassanetBillJob. Runs on the payments-medium queue.
  • PayKassanetBillJob (pay stage): Calls payKassanetBill() -> PayBill, recording the payment tender (Viva Wallet for kiosk, Stripe for QR/online ordering, falling back to the Webshop type) and the tip amount. On success the transaction's external_data.{provider}.status is set to success and need_requeue cleared. Runs on the payments-medium queue.

Unrecoverable Orders

Besides success and error, a transaction can land in a third, terminal state: external_data.{provider}.status = 'unrecoverable', with need_requeue = false, the raw errMessage, and an unrecoverable_at timestamp. It is never retried again — the requeue path refuses an unrecoverable transaction outright — and needs manual review.

An order is marked unrecoverable when:

  • GetBill returns a €0 total for a non-zero transaction (the POS table was cleared).
  • A retry finds a prior CreateOrder attempt left a non-zero bill that does not match the transaction total (partial/dirty guest check) — re-sending would compound the mess.
  • The create or pay stage fails with a terminal POS error: a table error such as "Tafel is leeg" / "table does not exist" / "invalid table number", or a permanent POS error code (e.g. 44 PLU not found, 77 price overwrite not allowed, 262 no booking possible/sold out, 312/321/341, PLU-filter errors).

Each one raises a [POS UNRECOVERABLE] Transaction requires manual review log entry (routed to Slack) and adds an [UNRECOVERABLE] Order #... line to the integration logs. For POS error 77 the log also carries a remediation hint: it is a POS-side fix — check the PLU price-override settings on the POS (often the order-type marker PLUs 4001/4002/4003) or update VPOS firmware to 6.3.8.0 or later.

Recovery and Observability

  • Stuck Transactions screen (/stuck-transactions): a back-office list of POS transactions that never completed, backed by GET /api/back-office/global/stuck-transactions and gated by the VIEW_INTEGRATION_MONITORING permission. Per row it offers retry sync, resolve and mark success.
  • php artisan kassanet:reconcile: a read-only report of Kassanet orders that reached the POS carrying only the order-type PLU marker and no priced lines. Each order is classified SELF_HEALED (the POS bill already matches, do not replay), AMBIGUOUS (a non-zero bill that does not match), NEEDS_ITEMS (no item records to replay) or REPLAYABLE. Options: --since, --merchant, --provider, --csv. It never repairs anything — it produces the list to confirm with the merchant.
  • php artisan pos:resolve-stuck-transaction {vendor_id}: the repair step run after that confirmation. Takes --transaction_id or --order_no and --action=inspect|mark-success|re-create (default inspect), with an optional --provider override.
  • php artisan kassanet:backfill-stable-keys: backfills stable_key on already-synced Kassanet items, modifiers, modifier groups and categories (--provider, --location, --dry-run).
  • php artisan orders:process-missing-kassanet-jobs --dry-run: legacy sweep for incomplete Kassanet transactions — deprecated in favour of the scheduled pos:retry-failed-order-syncs.

Fields

Credentials (stored in ThirdPartyIntegration.credentials)

FieldTypeDescription
base_urlstringKassanet API endpoint URL.
signature_keystringHMAC-SHA256 signature key for authentication.
encryption_tokenstringBlowfish CBC encryption key.
enable_encryptionboolWhether requests are encrypted. Defaults to true; not set via the connect form.

Settings (stored in ThirdPartyIntegration.settings)

FieldTypeDescription
sync_timestringDaily auto-sync time as a local wall-clock (HH:00 or HH:30), not UTC.
sync_timezonestringIANA zone sync_time is expressed in. Defaults to the location's timezone on first write, falling back to Europe/Brussels. Read back for display; never null.
next_run_timestringNext scheduled sync run, as an absolute UTC instant (managed by the scheduler).
tablesarrayTable/guest-check numbers retrieved from TableNumbers.
tables_usageobjectPer-table usage map (last_used, transaction_id).
webshop_payment_typeobjectCached Webshop payment type ({Number, Name}) from the POS.
cod_payment_methodstringCash-on-Delivery payment type name to resolve on the POS (default cash).

Status / sync fields (on ThirdPartyIntegration)

FieldTypeDescription
sync_status.statusstringsuccess, error, or in_progress.
sync_status.messagestringHuman-readable sync message.
sync_status.detailsobjectHolds display_groups_imported, menu_id.
sync_status.last_syncstringISO 8601 timestamp of the last import.
sync_status.statsobjectproductsCount, ordersCount (total/queued/failed/success), successRate, lastSync.
sync_status.logsarrayRecent log entries (info/success/error), capped.
sync_status.connection_statusobject{status, text, lastChecked}.
last_successful_teststringISO 8601 timestamp of the last successful test.

Kassanet API endpoints called

EndpointMethodDescription
testGETTest connectivity / read SystemInfo.
GetAllProductsGETFetch all categories, subcategories, products.
GetProductSelectionGETFetch product-selection (display group) definitions.
GetCountdownPluStateGETFetch stock levels (countdown PLUs).
TableNumbersGETFetch available table/guest-check numbers.
GetFreeTableNumberGETGet the next free table number for an order.
GetBill?tableNumber=GETRetrieve the bill for a table number.
GetPaymentTypesGETFetch available payment types.
CreateOrderPOSTRegister order line items on a table.
PayBillPOSTRecord payment tender against the bill.
PostDepositPOSTRecord a deposit/partial payment (used by split-payment tooling).

The exact request/response payloads and the full table-assignment and modifier-merge internals are not fully verified here.

Business Rules

  • A location may have only one in-house POS channel. Connecting Hendrickx is blocked if Vanhoutte (or another in-house POS such as ShopCaisse/MplusKassa) is already connected for that location/merchant, and vice versa — you cannot have both on the same location.
  • Connecting requires a branding profile; payment and billing profiles are optional.
  • The import chain jobs use 3 retry attempts with a 30-second backoff; per-job timeout is 300 seconds, except the products job which allows 600 seconds.
  • Order-push table assignment uses MongoDB-based distributed locking (per location + table) with a 300-second (5-minute) lock timeout to prevent two orders claiming the same table; up to 50 attempts are made to find a free table.
  • API requests use exponential backoff for retryable errors (timeouts, connection failures, HTTP 500/502/503/504/408). A non-retryable or final failure marks the location disconnected.
  • When a request fails because the POS is unreachable, the location is marked "disconnected" and affected orders are flagged need_requeue. A successful test (manual, via the kassanet:check-and-requeue command, or scheduled) marks the location connected and requeues those orders from their last incomplete stage.
  • A bill-total vs transaction-total mismatch is logged as a critical error but does not block payment; a €0 bill against a non-zero transaction marks the order unrecoverable.
  • An order marked unrecoverable is terminal: retries are refused, it does not requeue on reconnect, and it must be resolved manually.
  • Scheduled recovery tasks: kassanet:check-and-requeue and pos:retry-failed-order-syncs run every 5 minutes, and pos:reconcile-stuck-transactions runs hourly (plus a daily --prune pass at 03:00).

FAQs

What is the difference between Kassanet, Hendrickx, and Vanhoutte?

Kassanet is the underlying POS platform/API. Hendrickx and Vanhoutte are the two POS fascia that run on it. In Upvendo you connect via the Hendrickx or Vanhoutte page; both use the identical Kassanet protocol and the same code, differing only by the {provider} name.

Can I create menu items in Upvendo and push them to Kassanet?

No. Menu sync is one-way from Kassanet to Upvendo. Items must exist in the POS register first; you can then customize descriptions, images, and display order in Upvendo.

Does Kassanet process the customer's payment?

No — Upvendo collects the money via Viva Wallet (kiosk) or Stripe (online/QR ordering). Kassanet's role at the pay stage is to record the payment tender on the POS bill (via PayBill) so the POS shows the order as settled. The POS must have the matching payment type configured.

What happens when the POS register goes offline?

The order's transaction is flagged external_data.{provider}.need_requeue = true and the location is marked disconnected. When connectivity is restored (detected via the test endpoint), flagged orders are automatically reprocessed from the stage that failed (create, bill, or pay).

How does auto-sync work?

If you set a daily sync time (HH:00 or HH:30), a full menu import runs at that time every day. The time is your location's local time and is stored as such, so it stays put across daylight-saving changes. The scheduler manages next_run_time.

Why are some items missing after sync?

Placeholder items are removed after import: kitchen_name of zz, a * pager * pattern, or a numeric kitchen name equal to the default name. Also confirm items are assigned to a product selection (display group) on the POS.

Can I disconnect a Hendrickx/Vanhoutte integration myself?

No. The disconnect endpoint is intentionally disabled and returns an error asking you to contact the administrator.

Troubleshooting

"Connection test failed" or "Disconnected" status

Verify the Connection URL is correct and reachable, and that the Signature Key and Encryption Token match the POS register configuration. Ensure the POS register is powered on and online.

"Invalid base64 input" or "Blowfish decryption error"

The Encryption Token is incorrect or has changed, or enable_encryption does not match the POS configuration. Update the credentials with the correct token.

The import chain may have failed at a stage. Check the integration logs on the Hendrickx/Vanhoutte page. Each job retries 3 times (30s backoff); timeouts are 300s (600s for products). If it keeps failing, check the POS API responsiveness.

"Total amount mismatch" in logs

The GetBill total from Kassanet does not match the transaction total in Upvendo. This is logged as a critical error but does not block payment. Check that POS prices match Upvendo prices and look for tax/rounding differences. A €0 bill against a non-zero transaction marks the order unrecoverable (the POS table was likely cleared).

Orders not appearing / not settled in the POS

Check the connection status (should be "connected"). Inspect external_data.{provider} on the transaction — status is success, error or unrecoverable; the errMessage, need_requeue flag, and which of create / bill / pay is missing tell you the failed stage. If the POS was offline, error orders requeue automatically once a test succeeds; otherwise use the back-office retry-sync action. An unrecoverable order will not requeue — not on reconnect, not via retry-sync — and needs manual review (look for the [POS UNRECOVERABLE] log entry and the [UNRECOVERABLE] Order #... line in the integration logs). For POS error 77 the fix is on the POS: PLU price-override settings or VPOS firmware 6.3.8.0+.

Assistant Guidance

When a user asks about Kassanet, Hendrickx, or Vanhoutte integration:

  • Determine which provider they use (Hendrickx or Vanhoutte) — the behaviour is identical, only the name differs.
  • For connection issues, ask for the Connection URL and whether the POS register is online, and suggest a test connection.
  • For menu sync issues, check the sync status and logs on the integration page; the import runs as chained background jobs and can take a few minutes.
  • For order issues, check the connection status and the transaction's external_data.{provider} (status, errMessage, need_requeue, and which of create/bill/pay completed).
  • Clarify that Upvendo collects the payment (Viva Wallet for kiosk, Stripe for online/QR); Kassanet records the tender on the POS bill.
  • Disconnecting is disabled in the UI/API — direct users to contact an administrator.
  • Table numbers are virtual POS guest-check slots used for order push (with 5-minute distributed locks), not Upvendo in-house table sections.
  • The CLI command php artisan app:import-kassanet {locationId} --provider={hendrickx|vanhoutte} triggers a manual import (it dispatches the category import job directly).
  • For stuck orders, the back-office retry-sync re-dispatches the missing stage; the kassanet:check-and-requeue console command requeues across reconnected locations. The Stuck Transactions screen (/stuck-transactions, permission VIEW_INTEGRATION_MONITORING) lists them with retry-sync / resolve / mark-success actions.
  • For a suspected marker-only push (order on the POS with no priced lines), run kassanet:reconcile first — it only reports (SELF_HEALED / AMBIGUOUS / NEEDS_ITEMS / REPLAYABLE) and the verdict has to be confirmed with the merchant before repairing with pos:resolve-stuck-transaction. Never replay blindly: staff may already have keyed the items into the POS by hand.
  • If a transaction's status is unrecoverable, stop suggesting retries — it will not requeue. It needs manual review, and for POS error 77 the fix is on the POS side (PLU price-override settings / VPOS firmware 6.3.8.0+).

Relations

Depends On

  • Locations (a location must exist before connecting).
  • Branding profile (required at connect time).
  • Payment providers (Viva Wallet / Stripe collect the money; Kassanet records the tender).
  • ThirdPartyIntegration model (stores credentials, settings, sync status, logs).

Affects

  • Menu Items (created/updated during import).
  • Categories (created/updated during import).
  • Display Groups (created from product selections).
  • Modifier Groups and Modifiers (created from POS modifier data).
  • Inventory (stock levels synced from countdown PLUs).
  • Transactions (orders pushed to the POS via the create/bill/pay jobs; external_data.{provider} holds the stage results and status).