Loyalty+ API Reference
Full REST coverage for POS integrations, owner tools, and public customer flows.
Authentication Model
POS Integrations
X-API-Key
Use header X-API-Key for /v1/pos/* routes. API key belongs to a tenant.
Owner API
Bearer JWT
Use /v1/owner/login, then send Authorization: Bearer <token> for /v1/owner/*.
Customer Device APIs
X-Device-Id or deviceId
Public customer routes use hashed device identity in header or body, depending on endpoint.
API keys are valid only while tenant subscription is current (`active` or valid `trialing`). Expired trial or inactive subscriptions return 402 SUBSCRIPTION_REQUIRED.
Base URL: https://api.loyaltyplus.app
Third-Party Integration Quick Start
1) Owner Authentication
Use /v1/owner/login with owner email/password and JWT only for owner-management routes.
2) Generate Tenant API Key
Create or rotate API key from owner UI (Settings) or /v1/owner/api-key/generate.
3) POS Runtime Calls
Use only X-API-Key for /v1/pos/*. Do not send owner passwords from POS devices.
🩺Service
Health and platform-level routes
/healthNo authBasic health check endpoint.
Sample Response
{
"status": "ok"
}📡POS API
Primary third-party integration surface for cash registers and POS systems
/v1/pos/checkinX-API-KeyAward points to a customer by phone or customerId.
Request Body
{
"phone": "+353123456789",
"storeId": "optional_store_id",
"amount": 25.5,
"units": 3
}Sample Response
{
"success": true,
"customerId": "cuid_xxx",
"pointsAwarded": 25,
"totalPoints": 125,
"rewardThreshold": 100,
"rewardAvailable": true
}- • Use phone OR customerId.
- • For SPEND mode provide amount. For UNIT mode provide units.
/v1/pos/customer/:identifierX-API-KeyGet customer profile and reward availability by phone or customer ID.
Sample Response
{
"customerId": "cuid_xxx",
"name": "John Doe",
"phone": "+353123456789",
"status": "ACTIVE",
"totalPoints": 125,
"rewardThreshold": 100,
"rewardAvailable": true
}/v1/pos/balance/:identifierX-API-KeyQuick points balance lookup.
Sample Response
{
"customerId": "cuid_xxx",
"totalPoints": 125
}/v1/pos/redeemX-API-KeyRedeem customer points immediately from POS.
Request Body
{
"phone": "+353123456789",
"points": 100
}Sample Response
{
"success": true,
"redemptionId": "cuid_xxx",
"pointsRedeemed": 100,
"rewardTitle": "Free Coffee",
"remainingPoints": 25
}/v1/pos/customers/exportX-API-KeyExport tenant customers in JSON or CSV for POS-side sync.
Query Params
{
"format": "json | csv",
"page": 1,
"limit": 100
}Sample Response
{
"customers": [
{
"id": "...",
"name": "...",
"phone": "+353..."
}
],
"pagination": {
"page": 1,
"limit": 100,
"total": 1200,
"totalPages": 12
}
}/v1/pos/customers/importX-API-KeyBulk import customers into tenant from POS.
Request Body
{
"customers": [
{
"name": "John",
"phone": "+353123456789",
"points": 50
}
]
}Sample Response
{
"success": true,
"imported": 1,
"skipped": 0,
"errors": []
}🔐Owner Auth & Tenant
Signup, login, tenant profile, and API key lifecycle
/v1/owner/signupNo authCreate owner account + tenant + default store + default reward rule.
Request Body
{
"email": "owner@example.com",
"password": "StrongPass123",
"tenantName": "Main Store",
"businessType": "CAFE"
}Sample Response
{
"tenantId": "cuid_tenant",
"ownerId": "cuid_owner",
"storeId": "cuid_store"
}/v1/owner/loginNo authAuthenticate owner and return JWT.
Request Body
{
"email": "owner@example.com",
"password": "StrongPass123"
}Sample Response
{
"token": "jwt_token_here",
"tenantId": "cuid_tenant"
}/v1/owner/tenantBearer JWT (owner)Get tenant profile including subscription and dial-code defaults.
/v1/owner/tenantBearer JWT (owner)Update tenant settings.
Request Body
{
"phoneDefaultCountryDialCode": "+353"
}/v1/owner/accountBearer JWT (owner)Delete tenant account and related data.
/v1/owner/api-keyBearer JWT (owner)Get masked API key state.
/v1/owner/api-key/generateBearer JWT (owner)Generate or rotate API key (full key returned once).
Request Body
{
"name": "POS Integration Key"
}/v1/owner/api-keyBearer JWT (owner)Revoke current API key.
👥Owner Customers
CRUD, points adjustments, import/export
/v1/owner/customersBearer JWT (owner)Create customer manually.
Request Body
{
"name": "John Doe",
"phone": "+353123456789",
"notes": "VIP",
"initialPoints": 50
}/v1/owner/customersBearer JWT (owner)Paginated customer list with filtering/sorting.
Query Params
{
"page": 1,
"limit": 20,
"search": "john",
"statusFilter": "ALL | ACTIVE | BLOCKED",
"sortBy": "points | lastVisit | name | createdAt",
"sortOrder": "asc | desc"
}/v1/owner/customers/:idBearer JWT (owner)Customer detail including points history, visits, redemptions.
/v1/owner/customers/:idBearer JWT (owner)Update customer profile/status.
Request Body
{
"name": "Jane Doe",
"phone": "+353987654321",
"notes": "Frequent buyer",
"status": "ACTIVE"
}/v1/owner/customers/:id/pointsBearer JWT (owner)Manual points adjustment with audit trail.
Request Body
{
"points": -50,
"reason": "Refund",
"storeId": "optional_store_id"
}/v1/owner/customers/:idBearer JWT (owner)Permanently remove customer.
/v1/owner/customers/exportBearer JWT (owner)Export customers in JSON or CSV.
Query Params
{
"format": "json | csv",
"page": 1,
"limit": 1000,
"all": true
}/v1/owner/customers/importBearer JWT (owner)Bulk import customers.
Request Body
{
"customers": [
{
"name": "John",
"phone": "+353123456789",
"email": "john@example.com",
"points": 20
}
]
}🏬Owner Stores
Store CRUD and QR token issuance
/v1/owner/storesBearer JWT (owner)Create store.
Request Body
{
"name": "Main Store",
"timezone": "Europe/Dublin",
"posPin": "1234"
}/v1/owner/storesBearer JWT (owner)List stores.
/v1/owner/stores/:storeIdBearer JWT (owner)Get single store.
/v1/owner/stores/:storeIdBearer JWT (owner)Update store.
Request Body
{
"name": "Renamed Store",
"timezone": "UTC",
"posPin": "9876"
}/v1/owner/stores/:storeIdBearer JWT (owner)Delete store.
/v1/owner/stores/:storeId/qr-tokenBearer JWT (owner)Generate short-lived check-in QR token for a store.
Sample Response
{
"token": "signed_qr_token"
}🎁Owner Rewards
Reward rules and optional reward tiers
/v1/owner/reward-rulesBearer JWT (owner)List reward rules.
/v1/owner/reward-rulesBearer JWT (owner)Create reward rule.
Request Body
{
"storeId": "optional_store_id",
"earningMode": "VISIT",
"pointsPerVisit": 10,
"pointsRewardThreshold": 100,
"rewardTitle": "Free Reward",
"cooldownMinutesPerCheckin": 60
}/v1/owner/reward-rules/:ruleIdBearer JWT (owner)Get rule by ID.
/v1/owner/reward-rules/:ruleIdBearer JWT (owner)Update rule fields.
Request Body
{
"earningMode": "SPEND",
"pointsPerSpend": 1,
"spendThreshold": 1,
"pointsRewardThreshold": 120
}/v1/owner/reward-rules/:ruleIdBearer JWT (owner)Delete rule (cannot delete only default rule).
/v1/owner/reward-tiersBearer JWT (owner)List reward tiers.
/v1/owner/reward-tiersBearer JWT (owner)Create reward tier.
Request Body
{
"pointsRequired": 50,
"rewardTitle": "Small Reward",
"sortOrder": 0
}/v1/owner/reward-tiers/:tierIdBearer JWT (owner)Update reward tier.
Request Body
{
"pointsRequired": 100,
"rewardTitle": "Free Coffee",
"sortOrder": 1
}/v1/owner/reward-tiers/:tierIdBearer JWT (owner)Delete reward tier.
✅Owner Redemptions
Confirm and inspect redemptions
/v1/owner/redemptions/confirmBearer JWT (owner)Confirm started redemption by redeem code.
Request Body
{
"redeemCode": "AB12CD34"
}/v1/owner/redemptions/by-code/:redeemCodeBearer JWT (owner)Lookup redemption metadata by code.
/v1/owner/redemptionsBearer JWT (owner)Recent redemptions list.
📊Owner Dashboard
Business metrics and activation tracking
/v1/owner/dashboard/statsBearer JWT (owner)Dashboard metrics for owner UI.
/v1/owner/dashboard/activation-reportBearer JWT (owner)CSV activation report (signups vs first check-in in 24h).
Query Params
{
"days": 7
}🧠Owner AI Insights
Segments, churn, CLV, anomalies, analytics refresh
/v1/owner/insights/summaryBearer JWT (owner)Insights summary cards.
/v1/owner/insights/segmentsBearer JWT (owner)Segment distribution.
/v1/owner/insights/segments/:segment/customersBearer JWT (owner)Customers in a segment.
Query Params
{
"page": 1,
"limit": 20
}/v1/owner/insights/churnBearer JWT (owner)Churn risk list.
Query Params
{
"riskLevel": "HIGH",
"page": 1,
"limit": 20
}/v1/owner/insights/clvBearer JWT (owner)CLV prediction list.
Query Params
{
"tier": "HIGH",
"page": 1,
"limit": 20
}/v1/owner/insights/anomaliesBearer JWT (owner)Anomaly feed.
Query Params
{
"acknowledged": false,
"page": 1,
"limit": 20
}/v1/owner/insights/anomalies/:id/acknowledgeBearer JWT (owner)Mark anomaly acknowledged.
/v1/owner/insights/refreshBearer JWT (owner)Trigger analytics refresh (rate-limited server-side).
/v1/owner/insights/customer/:idBearer JWT (owner)Per-customer insight detail.
📱Public Customer App API
Endpoints used by QR check-in, /me, cashier, registration, and customer device flows
/v1/public/checkinsNo authQR check-in by token and deviceId.
Request Body
{
"token": "signed_qr_token",
"deviceId": "device-uuid"
}/v1/public/tenantsX-Device-Id / deviceIdList tenants linked to current device hash.
Headers
{
"X-Device-Id": "device-uuid"
}/v1/public/meX-Device-Id / deviceIdGet customer profile and balances for current device.
Headers
{
"X-Device-Id": "device-uuid"
}Query Params
{
"tenantId": "optional_tenant_id"
}/v1/public/historyX-Device-Id / deviceIdPaginated points activity history.
Headers
{
"X-Device-Id": "device-uuid"
}Query Params
{
"tenantId": "optional_tenant_id",
"page": 1,
"limit": 20,
"filter": "ALL | EARN | REDEEM | ADJUST"
}/v1/public/stores/:storeId/tokenNo authGet fresh QR token for printed/scanned store links.
/v1/public/cashier-checkinNo authCashier awards points by customer/store.
Request Body
{
"customerId": "cuid_customer",
"storeId": "cuid_store",
"amount": 20,
"units": 2
}/v1/public/customer-lookupNo authLookup customer by phone + store context for cashier flow.
Request Body
{
"phone": "0851234567",
"storeId": "cuid_store"
}/v1/public/redemptions/startNo authStart customer redemption and generate short code.
Request Body
{
"deviceId": "device-uuid",
"tenantId": "cuid_tenant",
"storeId": "optional_store_id",
"tierId": "optional_tier_id"
}/v1/public/redemptions/regenerateNo authExpire active code and issue fresh redemption code.
Request Body
{
"deviceId": "device-uuid",
"tenantId": "cuid_tenant",
"storeId": "optional_store_id"
}/v1/public/cashier-redemptions/redeemNo authCashier directly redeems reward for a customer.
Request Body
{
"storeId": "cuid_store",
"customerId": "cuid_customer"
}/v1/public/cashier-redemptions/confirm-codeNo authCashier confirms customer redeem code.
Request Body
{
"storeId": "cuid_store",
"redeemCode": "AB12CD34"
}/v1/public/redemptionsX-Device-Id / deviceIdList redemptions for current device and tenant.
Headers
{
"X-Device-Id": "device-uuid"
}Query Params
{
"tenantId": "cuid_tenant",
"page": 1,
"limit": 20
}/v1/public/registerNo authRegister customer with phone/name and optional device link.
Request Body
{
"tenantId": "cuid_tenant",
"storeId": "optional_store",
"name": "Alex",
"phone": "0851234567",
"deviceId": "optional_device_uuid"
}/v1/public/register/:storeId/infoNo authRegistration/cashier store info and earning mode details.
/v1/public/card/:customerIdNo authPublic member card summary.
/v1/public/login-phoneNo authLink device to phone-based customer account.
Request Body
{
"phone": "0851234567",
"tenantId": "cuid_tenant",
"deviceId": "device-uuid"
}/v1/public/account/deleteNo authAnonymize account for device + tenant.
Request Body
{
"deviceId": "device-uuid",
"tenantId": "cuid_tenant"
}/v1/public/account/existsNo authCheck whether account exists for device + tenant.
Request Body
{
"deviceId": "device-uuid",
"tenantId": "cuid_tenant"
}🔁Password Reset
Owner password reset flow endpoints
/v1/auth/forgot-passwordNo authRequest password reset email.
Request Body
{
"email": "owner@example.com"
}/v1/auth/reset-passwordNo authSet new password with reset token.
Request Body
{
"token": "reset_token",
"password": "NewStrongPassword123"
}💻Code Examples
curl -X POST https://api.loyaltyplus.app/v1/pos/checkin \
-H "Content-Type: application/json" \
-H "X-API-Key: loy_your_api_key_here" \
-d '{
"phone": "+353123456789",
"storeId": "your_store_id",
"amount": 24.90
}'curl -X POST https://api.loyaltyplus.app/v1/owner/login \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.com",
"password": "StrongPass123"
}'curl -X GET "https://api.loyaltyplus.app/v1/owner/customers?page=1&limit=20" \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
curl -X POST https://api.loyaltyplus.app/v1/owner/api-key/generate \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"POS Integration Key"}'const response = await fetch('https://api.loyaltyplus.app/v1/pos/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.POS_API_KEY
},
body: JSON.stringify({
phone: '+353123456789',
amount: 24.90
})
});
const data = await response.json();const loginRes = await fetch('https://api.loyaltyplus.app/v1/owner/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'owner@example.com',
password: 'StrongPass123'
})
});
const { token } = await loginRes.json();
const customersRes = await fetch('https://api.loyaltyplus.app/v1/owner/customers?page=1&limit=20', {
headers: { Authorization: `Bearer ${token}` }
});
const customers = await customersRes.json();import requests
base_url = "https://api.loyaltyplus.app"
api_key = "loy_your_api_key_here"
payload = {
"phone": "+353123456789",
"storeId": "your_store_id",
"amount": 24.90
}
res = requests.post(
f"{base_url}/v1/pos/checkin",
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json=payload,
timeout=15
)
res.raise_for_status()
print(res.json())const res = await fetch('https://api.loyaltyplus.app/v1/pos/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.POS_API_KEY
},
body: JSON.stringify({ phone: '+353123456789', amount: 24.90 })
});
if (!res.ok) {
// API errors follow: { error: string, message: string }
const err = await res.json();
throw new Error(`${err.error}: ${err.message}`);
}
const data = await res.json();⚠️Common Errors
Error Response Shape
{
"error": "SUBSCRIPTION_REQUIRED",
"message": "Active subscription required"
}Typical Error Codes
{
"VALIDATION_ERROR": 400,
"UNAUTHORIZED": 401,
"SUBSCRIPTION_REQUIRED": 402,
"FORBIDDEN": 403,
"NOT_FOUND": 404,
"CONFLICT": 409,
"INTERNAL_ERROR": 500
}