Skip to main content

Kiosk and POS Configuration

Read first: Overview & Concepts · QR Code Loyalty Guide

This is the vendor-visible configuration reference for an In-Store Loyalty API integration. It explains the two configuration endpoints your POS or kiosk calls to fetch a sales outlet's loyalty settings, and how those settings change when and how you call the four in-store API operations. The loyalty operations themselves (getOneTimeSalt, processQrCode, orderClosed, refundOrder) live in the In-Store Loyalty API specification; the configuration service is not part of that contract — it has its own OpenAPI specification.

Configuration is delivered over two endpoints, used identically by every device type — a cashier POS or a self-service kiosk (the same segments serve both; an rKeeper kiosk uses the same rkeeper segment a cashier rKeeper terminal does):

  1. Credential endpointPOST /cloudvalidator-config-service/v1/pos/{pos} — fetched once at setup. Returns the outlet's encrypted in-store API key plus the legacy connection / POS-type fields. (Sections 1–2.)
  2. Configuration endpointGET /cloudvalidator-config-service/v1/config/{establishmentId}authenticated and pollable. Returns the full per-outlet loyalty configuration: connection, program rules, and the kiosk-facing presentation / legal settings. Your device re-fetches it on every startup and re-applies it whenever the revision increases, so admin-panel changes reach devices with no reprovisioning. (Section 3.)

The single source of field-level truth for both endpoints — parameters, the response envelopes, and every configuration field, with types, enums, and worked examples — is the OpenAPI specification:

This page explains why, when, and in what order; the spec is the definitive field-level reference. For the configuration endpoint (Section 3) the prose also walks through each field, since those settings drive the kiosk UX directly.

Note: The configuration service (cloudvalidator-config-service) is distinct from the in-store API that serves the four loyalty operations. It has its own base path, host, and OpenAPI specification, and is not part of the In-Store Loyalty API contract.

1. How configuration reaches your integration

A device needs two kinds of settings: a one-time credential (which in-store API instance to talk to, the outlet's establishment ID, and its API key), and the operational configuration (everything the brand tunes — earn rules, registration flows, locales, legal documents, banners). The credential is fetched once at setup; the operational configuration is polled on every startup so brand changes propagate without re-touching the device. Because the configuration endpoint is authenticated, each poll is preceded by a one-time-salt request to the in-store API — the salt is combined with the decrypted apiPrivateKey to build the AuthorizationToken (Section 3.1).

2. The credential endpoint — POST /cloudvalidator-config-service/v1/pos/{pos}

Send an HTTP POST, keyed by the required identifier query parameter, with the {pos} path segment your integration is registered as:

POST {configBaseUrl}/cloudvalidator-config-service/v1/pos/{pos}?identifier=OUTLET-001

Note: This request has no one-time-salt / AuthorizationToken handshake — the outlet identifier is the lookup key. {configBaseUrl} is the configuration service's scheme and host, provided by LoyaltyPlant during onboarding (e.g. https://release.loyaltyplant.com); the /cloudvalidator-config-service prefix is the service's fixed base path. {configBaseUrl} is distinct from the in-store API {baseUrl} used by the four loyalty operations (returned per outlet as the apiUrl field).

This endpoint is fetch-once: the service marks an outlet's credential as applied the first time it is successfully returned, and a second fetch for the same identifier returns accepted: false. Persist what you receive; accepted: false is not an error to retry — if you lost the credential, contact LoyaltyPlant to reset the outlet's applied state.

2.1 Parameters, the {pos} segment, and the response envelope

The request takes the {pos} path segment and selects the outlet by the required identifier query parameter. Two optional query parameters change the lookup: midIdentifier, when present, overrides identifier as the lookup key; and version, an optional plugin/integration version string accepted but not persisted. The service registers the segments generic, clover, iiko (Syrve), and rkeeper (Restera) — the product segments are the original product identifiers, so keep calling /cloudvalidator-config-service/v1/pos/iiko and /cloudvalidator-config-service/v1/pos/rkeeper; kiosks use the same segment as the matching POS (an rKeeper kiosk uses rkeeper). If you are a POS vendor building a new integration — not one of the existing Clover / Syrve / rKeeper products — use the generic segment (POST /cloudvalidator-config-service/v1/pos/generic); LoyaltyPlant confirms the segment for your integration during onboarding. An unrecognized segment returns accepted: false. The complete parameter list, the PosConfigResponse envelope ({ accepted, config }), and worked per-POS examples are defined under operation getPosConfig in the specification.

Note: The identifier must be a stable, globally-unique value that already lives in the POS — assigned once, at provisioning, and never overwritten afterwards (do not reuse a mutable field such as a display name or a value the POS regenerates on update/reinstall). The outlet is resolved by this value on every credential fetch, so if it changes the outlet can no longer be matched.

2.2 What the credential endpoint returns

The credential endpoint returns the bootstrap your integration needs to authenticate to the in-store API:

  • password — the outlet's in-store API key (your apiPrivateKey), returned encrypted. The same key is used to authenticate to the configuration endpoint (Section 3.1).
  • apiUrl, apiVersion, establishmentId, discountId, flow — connection and POS-behavior fields (and POS-type-specific extras for Clover / Syrve / Restera). These are documented by the GenericPOSConfiguration and per-POS schemas in the spec.

Note: password is encrypted with DESede (Triple DES) in CBC mode with PKCS5 padding, Base64-encoded. The shared key and initialization vector are fixed and shared with integrators; LoyaltyPlant provides the decryption routine during onboarding. The decrypted value is the apiPrivateKey used to build the AuthorizationToken (see the QR Code Loyalty Guide, Step 2).

3. The configuration endpoint — GET /cloudvalidator-config-service/v1/config/{establishmentId}

Once you hold the establishmentId (from Section 2), fetch the outlet's full loyalty configuration:

GET {configBaseUrl}/cloudvalidator-config-service/v1/config/{establishmentId}
AuthorizationToken: <token>

The response is the same { accepted, config } envelope (OutletConfigResponse). accepted: false (with config: null) here means the outlet is not bound or loyalty is not configured (guest mode) — never fetch-once. Only fields with a value are present.

3.1 Authentication

This endpoint is protected by the same AuthorizationToken scheme as the in-store loyalty API — SHA-256 of the outlet's decrypted apiPrivateKey (from the credential endpoint's password) combined with a one-time-salt. You already build this token for processQrCode; reuse it. The configuration payload itself never contains the key. A missing or invalid token returns 401.

3.2 Polling and the revision

The endpoint is not fetch-once — call it on every device startup. The service always returns the full configuration; the body carries a revision (a monotonic epoch-second stamp). The device should compare it to its cached copy and re-apply the configuration only when revision increases. This is how a brand updates legal texts, swaps an install link, adds a country, or toggles a flow in the admin panel and has every device pick up the change at its next start.

3.3 Configuration fields

The full structure and a worked example are in the OutletConfig schema (operation getOutletConfig). Field by field:

Connection.

  • apiUrl — the in-store API base URL this outlet calls.
  • loyaltyApiVersion — the in-store API protocol version (e.g. 1.7); append it to each operation path (/qr-code/{loyaltyApiVersion}). Because it is delivered here, version migration is a config change: once your app ships support for a new version, LoyaltyPlant updates this value and your devices switch on their next poll — no reprovisioning.
  • establishmentId — the LoyaltyPlant establishment (sales outlet) ID, sent on every runtime call.

Display basics.

  • currency / currencySymbol — ISO 4217 code and symbol, used to render the accrual rule and balances.
  • timezone — IANA timezone, for timestamps and acceptance audit logs.
  • locales{ enabled, default, fallback }: the ISO 639-1 languages this device offers, the default, and the language to fall back to when a translation is missing.

loyaltyEnabled — the master switch. When false, the device runs as a plain POS/kiosk: no points, no loyalty-card or reward scanning, no registration — and the entire loyalty block is ignored. Use it to pause loyalty at specific outlets without removing the integration.

loyalty.* — the program settings (read only when loyaltyEnabled is true):

  • loyaltyPointAccrualRules{ pointsPerCurrencyUnit, rounding }: how purchases convert to points. pointsPerCurrencyUnit: 0.04 means "0.04 points per unit of currency" (i.e. 1 point per 25 ₾); rounding is floor (down, the default), round (arithmetic), or ceil (up).
  • phoneNumberFlow{ enabled, flow }: whether a customer can join/identify by phone here, and how consent is collected. single_step shows phone + marketing consent on one screen (the runtime qr-code request's subscribedToPushNotifications reflects that single tap); two_step shows a separate consent screen after the phone screen (subscribedToPushNotifications reflects the second screen's answer — cleaner GDPR posture). Ignored when enabled is false.
  • loyaltyCardFlow{ enabled }: whether the customer can identify by scanning a loyalty QR card or entering its short code (vs. phone-only). Both reach the in-store API in the qrCode field; only the UX differs.
  • rewardRedemption{ enabled }: whether the customer can scan a reward/gift QR and apply it to the order.
  • pointsIconUrl — image URL for the points icon shown next to balances/accrual (not localized).
  • rewardBanner{ imageUrl }: an image promoting the rewards/gifts available at this outlet.
  • marketingConsent{ channels, screen }: the marketing channels the brand offers (sms, whatsapp, email, push) and the consent-screen copy. An empty channels means consent is not requested (and the two_step consent screen is not shown). screen.headlineByLocale / screen.bodyByLocale hold the copy per language; the body may contain {document_kind} placeholders (e.g. {privacy_policy}) listed in screen.documentRefs, which your device replaces with links to the matching documents[] entry.
  • phoneFormDisclaimer{ templateByLocale, documentRefs }: the fine-print under the phone-entry form, per language, shown in both flows. The {privacy_policy}, {terms_conditions}, {loyalty_terms}, {marketing_consent} placeholders are replaced with links to the documents named in documentRefs.
  • appInstallBanner{ url, imageUrl, promoTextByLocale, showWhen }: the "get the app" banner. url is the universal/dynamic install link you render as a QR code; promoTextByLocale is the per-language text next to it; showWhen is four booleans deciding which end-state shows the banner — justRegisteredWithApp, justRegisteredWithoutApp, alreadyRegistered, skipped (guest).

documents[] — legal documents: each entry has a kind (privacy_policy, terms_conditions, loyalty_terms, marketing_consent), a version (recorded in the consent audit trail when the customer accepts), titleByLocale, pdfUrlByLocale (immutable — a new version is a new URL), and requiresAcceptance (whether an explicit "I accept" tap is required, vs. informational). The documentRefs in the consent screen and the disclaimer point at these kinds.

phone — number entry. defaultCountry (ISO 3166-1 alpha-2) drives the dialing code and validation; allowedCountries is an optional list of extra accepted countries (e.g. for tourists); masksByCountry is an optional per-country input mask (X = a digit, other characters are literals), falling back to libphonenumber when a country is absent.

3.4 Worked example

A full accepted: true response for a loyalty-enabled outlet. Comments are annotations only — they are not part of the wire format, and only fields with a value are present:

{
"accepted": true,
"config": {
"revision": 1748186400, // monotonic change stamp; re-apply only when it increases
"apiUrl": "https://release.loyaltyplant.com/CloudValidator", // in-store API base URL for this outlet
"loyaltyApiVersion": "1.7", // pollable protocol version — append to each operation path
"establishmentId": 5259, // sent on every runtime call
"currency": "GEL", // ISO 4217 code
"currencySymbol": "₾",
"timezone": "Asia/Tbilisi", // IANA tz for timestamps / audit logs
"locales": { // languages this device offers
"enabled": ["en", "ka"],
"default": "en",
"fallback": "en" // used when a translation is missing
},
"loyaltyEnabled": true, // master switch — when false, the whole "loyalty" block is ignored
"loyalty": {
"loyaltyPointAccrualRules": {
"pointsPerCurrencyUnit": 0.04, // 0.04 points per unit of currency (1 point per 25 ₾)
"rounding": "floor" // floor (default) | round | ceil
},
"pointsIconUrl": "https://cdn.example.com/assets/points-icon.svg", // not localized
"phoneNumberFlow": {
"enabled": true,
"flow": "two_step" // single_step | two_step (separate consent screen)
},
"loyaltyCardFlow": { "enabled": true }, // identify by scanning a loyalty QR / short code
"rewardRedemption": { "enabled": true }, // scan a reward/gift QR and apply it
"rewardBanner": {
"imageUrl": "https://cdn.example.com/assets/rewards-banner.png"
},
"marketingConsent": {
"channels": ["sms", "whatsapp", "email", "push"], // empty ⇒ consent not requested
"screen": {
"headlineByLocale": {
"en": "Would you like to get exclusive deals to your phone number?",
"ka": "გსურთ მიიღოთ ექსკლუზიური შეთავაზებები?"
},
"bodyByLocale": {
// {document_kind} placeholders resolve to the matching documents[] entry
"en": "We'll send personalized offers based on your activity. See {privacy_policy}. Opt out anytime.",
"ka": "გამოგიგზავნით პერსონალურ შეთავაზებებს. იხ. {privacy_policy}."
},
"documentRefs": ["privacy_policy"]
}
},
"phoneFormDisclaimer": { // fine-print under the phone-entry form, per language
"templateByLocale": {
"en": "By providing your phone number, you agree to {privacy_policy}, {terms_conditions}, {loyalty_terms} and {marketing_consent}",
"ka": "ნომრის მითითებით თქვენ ეთანხმებით: {privacy_policy}, {terms_conditions}, {loyalty_terms}, {marketing_consent}"
},
"documentRefs": ["privacy_policy", "terms_conditions", "loyalty_terms", "marketing_consent"]
},
"appInstallBanner": {
"url": "https://brand.app.link/get", // universal/dynamic install link — render as a QR code
"imageUrl": "https://cdn.example.com/assets/app-banner.png",
"promoTextByLocale": {
"en": "Get the app — earn and spend points right from your phone",
"ka": "ჩამოტვირთეთ აპლიკაცია — დააგროვეთ ქულები"
},
"showWhen": { // which end-state shows the banner
"justRegisteredWithApp": true,
"justRegisteredWithoutApp": true,
"alreadyRegistered": true,
"skipped": false // guest
}
}
},
"documents": [ // legal documents referenced by documentRefs above
{
"kind": "privacy_policy",
"version": "2026-04-15", // recorded in the consent audit trail on acceptance
"titleByLocale": { "en": "Privacy Policy", "ka": "კონფიდენციალურობის პოლიტიკა" },
"pdfUrlByLocale": { // immutable — a new version is a new URL
"en": "https://cdn.example.com/docs/pp/en/2026-04-15.pdf",
"ka": "https://cdn.example.com/docs/pp/ka/2026-04-15.pdf"
},
"requiresAcceptance": true // explicit "I accept" tap required (vs. informational)
}
// ... terms_conditions, loyalty_terms, marketing_consent entries follow the same shape
],
"phone": {
"defaultCountry": "GE", // ISO 3166-1 alpha-2 — drives dialing code + validation
"allowedCountries": ["GE", "AM", "TR"], // optional extra accepted countries
"masksByCountry": { // optional input mask; X = a digit, other chars literal
"GE": "+995 XXX XXX XXX",
"AM": "+374 XX XXX XXX",
"TR": "+90 XXX XXX XX XX"
}
}
}
}

4. How configuration drives the flows

Two values from the credential endpoint change how you run the in-store API operations described in the QR Code Loyalty Guide: flow and discountId.

4.1 flow changes when you scan

The full in-store API cycle is always one-time-salt → qr-code → order-closed (with an optional refund). What flow decides is the moment the qr-code scan happens relative to closing the order:

  • REGULAR — the customer scans their QR code at the self-service kiosk, or the cashier scans the customer's QR code at the cash register / POS terminal (or, on the phone number-based flow, the customer enters their number on the kiosk or the cashier enters it at the POS / cash register) at any point while the order is open; apply the returned rewards and discounts, and call order-closed when the order is paid. This is the flow the QR Code Loyalty Guide walks through.
  • PATCH_ORDER — assemble the full order with reward items added as ordinary paid lines, then the customer scans their QR code at the self-service kiosk, or the cashier scans the customer's QR code at the cash register / POS terminal, immediately before closing so the validation applies the discounts that zero out the reward lines. Scanning early is not expected in this flow.

REGULAR (the default) and PATCH_ORDER are the values you will receive. The request and response schemas are identical for both flows — only your timing changes. Honor the flow your configuration returns; do not hard-code one.

4.2 discountId is the reward discount you echo

When processQrCode returns a reward item, it arrives with a 100% discount that brings the line to zero, carrying the code your outlet is configured with — the value of discountId. When you echo the reward back in order-closed, the zero-priced reward line must carry that same discountId; that is how the in-store API recognizes the reward was actually used and settles it. Using a different code (or dropping the discount) leaves the redemption unsettled.

Note: discountId is configured per outlet — it is not always the literal string "LoyaltyPlant". Read it from your configuration and echo exactly the value the in-store API sends on the reward's discount in the qr-code response.

5. Scope note

These configuration endpoints are consumed by your POS or kiosk integration to bootstrap and operate each outlet. They are served by a separate LoyaltyPlant service (cloudvalidator-config-service), not the in-store API, so they are not part of the In-Store Loyalty API specification (which is reserved for the four loyalty operations). Their machine-readable contract is the dedicated POS Configuration API specification; this page is the narrative companion to it, and where prose and the contract disagree, the spec is authoritative for field-level detail. The legacy Integration Server XML protocol is out of scope for this documentation set and is not described here.


Next: Versioning and Changelog →