Skip to main content
Download Markdown

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 endpoint — POST /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 endpoint — GET /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 →