Download OpenAPI specification:Download
The API enables POS systems to interact with LoyaltyPlant's loyalty program:
Returns a one-time salt and its ID used to compute the AuthorizationToken header
for subsequent API calls. Each salt can only be used once.
| establishmentId required | integer >= 1 LoyaltyPlant establishment ID. Should be configurable on the POS side. |
{- "establishmentId": 5259
}{- "saltId": 12345,
- "salt": "abcdefghijklmnop"
}Processes a LoyaltyPlant QR code scanned from the customer's mobile app,
identifies/creates a customer by phone number (in-store user acquisition),
or looks up an existing customer by publicClientId.
Returns reward items, discounts, and payments that should be applied to the order. Response includes customer tier name and points balance (v1.7+).
QR code flow: Send qrCode field with the scanned string.
Phone number flow (v1.6+): Send phoneNumber instead of qrCode,
along with optional subscribedToPushNotifications and clientName.
Public client ID flow (v1.7+): Send publicClientId instead of qrCode/phoneNumber
for direct client lookup by existing public ID.
Identification priority: if more than one of qrCode, phoneNumber, publicClientId
is sent, they are resolved in priority order qrCode → phoneNumber → publicClientId.
If none of the three is sent, the request is rejected with message code 503
(QR code invalid).
Customer tier and balance (v1.7+): customer.customerTier and
customer.customerPointsBalance are best-effort enrichment. If the tier/balance lookup
is unavailable, both fields are returned as null and the request is still accepted
(accepted: true) — treat their absence as "unknown", not as a failure.
Note: QR codes are valid for 5 minutes and can only be used once.
| SaltId required | integer Salt ID received from the |
| AuthorizationToken required | string SHA256 hash of
|
| qrCode | string QR code string from LoyaltyPlant mobile app. Valid for 5 minutes, single use.
Required if |
| phoneNumber | string Guest phone number in international format, e.g. |
| subscribedToPushNotifications | boolean Default: false If |
| clientName | string Client name for account creation. Only applicable with |
| publicClientId | integer or null Existing customer public ID. If provided, used for direct client lookup instead of QR code or phone number. The client must belong to the same partner. |
| orderId required | string Order identifier in POS. |
required | object (PosInfo) |
| total | number <decimal> Current order total. |
object (Order) Order contents with menu items and order-level discounts (v1.5+). |
{- "qrCode": "1234567878901234567890",
- "orderId": "1234567",
- "total": 1234.3,
- "posInfo": {
- "establishmentId": 1234,
- "terminalId": "1234",
- "terminalCurrentTimestamp": 1466337067000,
- "terminalTimeZone": "GMT-6"
}, - "order": {
- "menuItems": [
- {
- "itemId": "1234",
- "title": "Something",
- "price": 10.2,
- "quantity": 2
}
]
}
}{- "accepted": true,
- "menuItems": [
- {
- "itemId": "1234",
- "title": "Something",
- "price": 10.2,
- "quantity": 2
}
], - "discounts": [ ],
- "messages": [
- {
- "type": "info",
- "code": 201,
- "text": "Welcome guest by name: John"
}
], - "customer": {
- "loyaltyPlantId": 1234,
- "customerName": "John",
- "customerEmail": "johndoe@web.com",
- "customerTier": "Gold",
- "customerPointsBalance": 450
}, - "payment": {
- "amount": 2.34,
- "transactionId": 1234,
- "type": "PAY_WITH_POINTS"
}, - "hasEverRewardedPoints": true,
- "maxPointsPercent": 10
}Same as v1.6. Notifies LoyaltyPlant that the order has been paid and closed.
| SaltId required | integer Salt ID received from the |
| AuthorizationToken required | string SHA256 hash of
|
| phoneNumber | string Guest phone number (v1.6, in-store acquisition flow). Not present in the monolith order-closed contract. |
| subscribedToPushNotifications | boolean Push notification subscription flag (v1.6, in-store acquisition flow). Not present in the monolith order-closed contract. |
| clientName | string Client name (v1.6, in-store acquisition flow). Not present in the monolith order-closed contract. |
| establishmentId required | integer LoyaltyPlant establishment ID. |
| orderId required | string Order identifier in POS. |
| orderTotal required | number <decimal> Final order total. |
| pointsReward | integer Override points reward amount. Warning: usually should NOT be specified. Only use to force a specific number of bonus points instead of the calculated amount. |
required | object (Order) Order contents with menu items and order-level discounts (v1.5+). |
required | Array of objects (Payment) List of payments applied to the order. |
object (Waiter) |
{- "establishmentId": 1234,
- "orderId": "1234",
- "orderTotal": 12.35,
- "order": {
- "menuItems": [
- {
- "itemId": "1234",
- "title": "Something",
- "price": 10.2,
- "quantity": 2,
- "modifiers": [
- {
- "itemId": "1234",
- "title": "Modifier title",
- "quantity": 3,
- "price": 22.1
}
], - "discounts": [
- {
- "discountId": "1234",
- "type": "percent",
- "value": 50.4
}
]
}
], - "discounts": [
- {
- "discountId": "1234",
- "type": "amount",
- "value": 50.4
}
]
}, - "payments": [
- {
- "type": "1",
- "amount": 2.34,
- "transactionId": "1234"
}
], - "waiter": {
- "posId": "2134",
- "name": "John Doe"
}
}{- "accepted": true,
- "pointsReward": 10,
- "pointsSpent": 20,
- "errorCode": 0,
- "errorText": ""
}Same as v1.6. Notifies LoyaltyPlant that the order has been refunded.
| SaltId required | integer Salt ID received from the |
| AuthorizationToken required | string SHA256 hash of
|
| establishmentId required | integer LoyaltyPlant establishment ID. |
| orderId required | string Order identifier in POS. |
{- "establishmentId": 5259,
- "orderId": "68acf80c0d0a7e6d2fd19946"
}{- "accepted": true,
- "errorCode": 0,
- "errorText": ""
}Returns the loyalty configuration for one sales outlet, selected by the identifier
query parameter (or midIdentifier when present) and routed by the {pos} path segment.
The request has no body and no auth handshake (no one-time-salt /
AuthorizationToken), and always responds with HTTP 200 —
success or failure is conveyed by the accepted field of the response envelope.
accepted: true — the configuration is returned in config. The outlet is now marked
applied (fetch-once); subsequent fetches for the same identifier return accepted: false.
accepted: false (with config: null) when any of:
{pos} segment is not a registered handler.Persist the configuration you receive as the source of truth for the outlet;
accepted: false is not an error to retry.
version — optional plugin/integration version string; currently accepted but not
persisted by the service.
Note: the service also accepts the trailing-slash form POST /v1/pos/{pos}/.
| pos required | string Enum: "generic" "clover" "iiko" "rkeeper" POS-type segment that selects the configuration handler:
LoyaltyPlant assigns the segment during onboarding. The currently deployed service
registers |
| identifier required | string The outlet's lookup key for the configuration. 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 fetch, so if it changes the outlet can no longer be matched. |
| midIdentifier | string Alternative outlet-lookup identifier (a merchant/MID-level code) added for the
rkeeper integration, used in place of the standard |
| version | string Optional plugin/integration version string. Accepted but not persisted. |
| fullRestaurantCode | string Alternative outlet-lookup identifier added for the rkeeper integration, used in
place of the standard |
The generic POS type returns just the core fields every integration needs.
Only fields with a value are present.
{- "accepted": true,
- "config": {
- "apiVersion": "1.6",
- "establishmentId": 1234,
- "password": "p7Qm0Zr8Kx2bN5tV9wYcAg==",
- "discountId": "LOYALTY",
- "flow": "REGULAR"
}
}Returns the complete per-outlet loyalty configuration — connection, program rules, and the kiosk-facing presentation and legal settings. Used identically by every device type: a cashier POS reads the connection and program fields and ignores the presentation fields; a self-service kiosk uses all of them. Only fields with a value are present (null fields omitted).
Unlike POST /v1/pos/{pos}, this endpoint is not fetch-once — it is safe to call on every
device startup. Compare the returned revision (a monotonic epoch-second stamp) with your
cached copy and re-apply the configuration when revision increases; this is how admin-panel
changes reach devices without reprovisioning. accepted: false here means the outlet is not
bound / loyalty is not configured (guest mode) — never a fetch-once rejection.
Authentication. Protected by the same AuthorizationToken scheme as the In-Store Loyalty
API (SHA-256 of the outlet's decrypted apiPrivateKey + a one-time-salt). The apiPrivateKey
is obtained once from the password field of POST /v1/pos/{pos}; the configuration payload
itself never contains the key.
Caching. The response carries an ETag equal to the revision; send it back as
If-None-Match to get 304 Not Modified when nothing changed. The revision in the body is
the authoritative change signal; the ETag is a bandwidth optimization.
| establishmentId required | integer <int32> The LoyaltyPlant establishment (sales outlet) ID, obtained from |
| identifier | string Optional outlet identifier, accepted as a fallback lookup key for a device that has not yet cached its |
| If-None-Match | string An |
{- "accepted": true,
- "config": {
- "revision": 1748186400,
- "loyaltyApiVersion": "1.7",
- "establishmentId": 5259,
- "currency": "GEL",
- "currencySymbol": "₾",
- "timezone": "Asia/Tbilisi",
- "locales": {
- "enabled": [
- "en",
- "ka"
], - "default": "en",
- "fallback": "en"
}, - "loyaltyEnabled": true,
- "loyalty": {
- "loyaltyPointAccrualRules": {
- "pointsPerCurrencyUnit": 0.04,
- "rounding": "floor"
}, - "phoneNumberFlow": {
- "enabled": true,
- "flow": "two_step"
}, - "loyaltyCardFlow": {
- "enabled": true
}, - "rewardRedemption": {
- "enabled": true
}, - "marketingConsent": {
- "channels": [
- "sms",
- "whatsapp",
- "email",
- "push"
], - "screen": {
- "headlineByLocale": {
- "en": "Would you like to get exclusive deals to your phone number?",
- "ka": "გსურთ მიიღოთ ექსკლუზიური შეთავაზებები?"
}, - "bodyByLocale": {
- "en": "We'll send personalized offers based on your activity. See {privacy_policy}. Opt out anytime.",
- "ka": "გამოგიგზავნით პერსონალურ შეთავაზებებს. იხ. {privacy_policy}."
}, - "documentRefs": [
- "privacy_policy"
]
}
}, - "phoneFormDisclaimer": {
- "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": {
- "promoTextByLocale": {
- "en": "Get the app — earn and spend points right from your phone",
- "ka": "ჩამოტვირთეთ აპლიკაცია — დააგროვეთ ქულები"
}, - "showWhen": {
- "justRegisteredWithApp": true,
- "justRegisteredWithoutApp": true,
- "alreadyRegistered": true,
- "skipped": false
}
}
}, - "documents": [
- {
- "kind": "privacy_policy",
- "version": "2026-04-15",
- "titleByLocale": {
- "en": "Privacy Policy",
- "ka": "კონფიდენციალურობის პოლიტიკა"
}, - "pdfUrlByLocale": {
}, - "requiresAcceptance": true
}, - {
- "kind": "terms_conditions",
- "version": "2026-04-15",
- "titleByLocale": {
- "en": "Terms & Conditions",
- "ka": "წესები და პირობები"
}, - "pdfUrlByLocale": {
}, - "requiresAcceptance": true
}, - {
- "kind": "loyalty_terms",
- "version": "2026-04-15",
- "titleByLocale": {
- "en": "Loyalty Program Terms",
- "ka": "ლოიალურობის წესები"
}, - "pdfUrlByLocale": {
}, - "requiresAcceptance": true
}, - {
- "kind": "marketing_consent",
- "version": "2026-04-15",
- "titleByLocale": {
- "en": "Marketing Consent",
- "ka": "მარკეტინგული თანხმობა"
}, - "pdfUrlByLocale": {
}, - "requiresAcceptance": false
}
], - "phone": {
- "defaultCountry": "GE",
- "allowedCountries": [
- "GE",
- "AM",
- "TR"
], - "masksByCountry": {
- "GE": "+995 XXX XXX XXX",
- "AM": "+374 XX XXX XXX",
- "TR": "+90 XXX XXX XX XX"
}
}
}
}