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):
- 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.) - 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 therevisionincreases, 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:
- POS Configuration API specification (OpenAPI 3.0.3) — operations
getPosConfigandgetOutletConfig(provided separately by LoyaltyPlant during onboarding)
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/AuthorizationTokenhandshake — 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-serviceprefix 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 theapiUrlfield).
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
identifiermust 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 (yourapiPrivateKey), 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 theGenericPOSConfigurationand per-POS schemas in the spec.
Note:
passwordis 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 theapiPrivateKeyused to build theAuthorizationToken(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.04means "0.04 points per unit of currency" (i.e. 1 point per 25 ₾);roundingisfloor(down, the default),round(arithmetic), orceil(up).phoneNumberFlow—{ enabled, flow }: whether a customer can join/identify by phone here, and how consent is collected.single_stepshows phone + marketing consent on one screen (the runtimeqr-coderequest'ssubscribedToPushNotificationsreflects that single tap);two_stepshows a separate consent screen after the phone screen (subscribedToPushNotificationsreflects the second screen's answer — cleaner GDPR posture). Ignored whenenabledisfalse.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 theqrCodefield; 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 emptychannelsmeans consent is not requested (and thetwo_stepconsent screen is not shown).screen.headlineByLocale/screen.bodyByLocalehold the copy per language; the body may contain{document_kind}placeholders (e.g.{privacy_policy}) listed inscreen.documentRefs, which your device replaces with links to the matchingdocuments[]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 indocumentRefs.appInstallBanner—{ url, imageUrl, promoTextByLocale, showWhen }: the "get the app" banner.urlis the universal/dynamic install link you render as a QR code;promoTextByLocaleis the per-language text next to it;showWhenis 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 callorder-closedwhen 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:
discountIdis 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 theqr-coderesponse.
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.