Developer Docs

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

GET/healthNo auth

Basic health check endpoint.

Sample Response

{
  "status": "ok"
}

📡POS API

Primary third-party integration surface for cash registers and POS systems

POST/v1/pos/checkinX-API-Key

Award 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.
GET/v1/pos/customer/:identifierX-API-Key

Get 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
}
GET/v1/pos/balance/:identifierX-API-Key

Quick points balance lookup.

Sample Response

{
  "customerId": "cuid_xxx",
  "totalPoints": 125
}
POST/v1/pos/redeemX-API-Key

Redeem 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
}
GET/v1/pos/customers/exportX-API-Key

Export 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
  }
}
POST/v1/pos/customers/importX-API-Key

Bulk 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

POST/v1/owner/signupNo auth

Create 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"
}
POST/v1/owner/loginNo auth

Authenticate owner and return JWT.

Request Body

{
  "email": "owner@example.com",
  "password": "StrongPass123"
}

Sample Response

{
  "token": "jwt_token_here",
  "tenantId": "cuid_tenant"
}
GET/v1/owner/tenantBearer JWT (owner)

Get tenant profile including subscription and dial-code defaults.

PATCH/v1/owner/tenantBearer JWT (owner)

Update tenant settings.

Request Body

{
  "phoneDefaultCountryDialCode": "+353"
}
DELETE/v1/owner/accountBearer JWT (owner)

Delete tenant account and related data.

GET/v1/owner/api-keyBearer JWT (owner)

Get masked API key state.

POST/v1/owner/api-key/generateBearer JWT (owner)

Generate or rotate API key (full key returned once).

Request Body

{
  "name": "POS Integration Key"
}
DELETE/v1/owner/api-keyBearer JWT (owner)

Revoke current API key.

👥Owner Customers

CRUD, points adjustments, import/export

POST/v1/owner/customersBearer JWT (owner)

Create customer manually.

Request Body

{
  "name": "John Doe",
  "phone": "+353123456789",
  "notes": "VIP",
  "initialPoints": 50
}
GET/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"
}
GET/v1/owner/customers/:idBearer JWT (owner)

Customer detail including points history, visits, redemptions.

PATCH/v1/owner/customers/:idBearer JWT (owner)

Update customer profile/status.

Request Body

{
  "name": "Jane Doe",
  "phone": "+353987654321",
  "notes": "Frequent buyer",
  "status": "ACTIVE"
}
POST/v1/owner/customers/:id/pointsBearer JWT (owner)

Manual points adjustment with audit trail.

Request Body

{
  "points": -50,
  "reason": "Refund",
  "storeId": "optional_store_id"
}
DELETE/v1/owner/customers/:idBearer JWT (owner)

Permanently remove customer.

GET/v1/owner/customers/exportBearer JWT (owner)

Export customers in JSON or CSV.

Query Params

{
  "format": "json | csv",
  "page": 1,
  "limit": 1000,
  "all": true
}
POST/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

POST/v1/owner/storesBearer JWT (owner)

Create store.

Request Body

{
  "name": "Main Store",
  "timezone": "Europe/Dublin",
  "posPin": "1234"
}
GET/v1/owner/storesBearer JWT (owner)

List stores.

GET/v1/owner/stores/:storeIdBearer JWT (owner)

Get single store.

PATCH/v1/owner/stores/:storeIdBearer JWT (owner)

Update store.

Request Body

{
  "name": "Renamed Store",
  "timezone": "UTC",
  "posPin": "9876"
}
DELETE/v1/owner/stores/:storeIdBearer JWT (owner)

Delete store.

GET/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

GET/v1/owner/reward-rulesBearer JWT (owner)

List reward rules.

POST/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
}
GET/v1/owner/reward-rules/:ruleIdBearer JWT (owner)

Get rule by ID.

PATCH/v1/owner/reward-rules/:ruleIdBearer JWT (owner)

Update rule fields.

Request Body

{
  "earningMode": "SPEND",
  "pointsPerSpend": 1,
  "spendThreshold": 1,
  "pointsRewardThreshold": 120
}
DELETE/v1/owner/reward-rules/:ruleIdBearer JWT (owner)

Delete rule (cannot delete only default rule).

GET/v1/owner/reward-tiersBearer JWT (owner)

List reward tiers.

POST/v1/owner/reward-tiersBearer JWT (owner)

Create reward tier.

Request Body

{
  "pointsRequired": 50,
  "rewardTitle": "Small Reward",
  "sortOrder": 0
}
PUT/v1/owner/reward-tiers/:tierIdBearer JWT (owner)

Update reward tier.

Request Body

{
  "pointsRequired": 100,
  "rewardTitle": "Free Coffee",
  "sortOrder": 1
}
DELETE/v1/owner/reward-tiers/:tierIdBearer JWT (owner)

Delete reward tier.

Owner Redemptions

Confirm and inspect redemptions

POST/v1/owner/redemptions/confirmBearer JWT (owner)

Confirm started redemption by redeem code.

Request Body

{
  "redeemCode": "AB12CD34"
}
GET/v1/owner/redemptions/by-code/:redeemCodeBearer JWT (owner)

Lookup redemption metadata by code.

GET/v1/owner/redemptionsBearer JWT (owner)

Recent redemptions list.

📊Owner Dashboard

Business metrics and activation tracking

GET/v1/owner/dashboard/statsBearer JWT (owner)

Dashboard metrics for owner UI.

GET/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

GET/v1/owner/insights/summaryBearer JWT (owner)

Insights summary cards.

GET/v1/owner/insights/segmentsBearer JWT (owner)

Segment distribution.

GET/v1/owner/insights/segments/:segment/customersBearer JWT (owner)

Customers in a segment.

Query Params

{
  "page": 1,
  "limit": 20
}
GET/v1/owner/insights/churnBearer JWT (owner)

Churn risk list.

Query Params

{
  "riskLevel": "HIGH",
  "page": 1,
  "limit": 20
}
GET/v1/owner/insights/clvBearer JWT (owner)

CLV prediction list.

Query Params

{
  "tier": "HIGH",
  "page": 1,
  "limit": 20
}
GET/v1/owner/insights/anomaliesBearer JWT (owner)

Anomaly feed.

Query Params

{
  "acknowledged": false,
  "page": 1,
  "limit": 20
}
POST/v1/owner/insights/anomalies/:id/acknowledgeBearer JWT (owner)

Mark anomaly acknowledged.

POST/v1/owner/insights/refreshBearer JWT (owner)

Trigger analytics refresh (rate-limited server-side).

GET/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

POST/v1/public/checkinsNo auth

QR check-in by token and deviceId.

Request Body

{
  "token": "signed_qr_token",
  "deviceId": "device-uuid"
}
GET/v1/public/tenantsX-Device-Id / deviceId

List tenants linked to current device hash.

Headers

{
  "X-Device-Id": "device-uuid"
}
GET/v1/public/meX-Device-Id / deviceId

Get customer profile and balances for current device.

Headers

{
  "X-Device-Id": "device-uuid"
}

Query Params

{
  "tenantId": "optional_tenant_id"
}
GET/v1/public/historyX-Device-Id / deviceId

Paginated points activity history.

Headers

{
  "X-Device-Id": "device-uuid"
}

Query Params

{
  "tenantId": "optional_tenant_id",
  "page": 1,
  "limit": 20,
  "filter": "ALL | EARN | REDEEM | ADJUST"
}
GET/v1/public/stores/:storeId/tokenNo auth

Get fresh QR token for printed/scanned store links.

POST/v1/public/cashier-checkinNo auth

Cashier awards points by customer/store.

Request Body

{
  "customerId": "cuid_customer",
  "storeId": "cuid_store",
  "amount": 20,
  "units": 2
}
POST/v1/public/customer-lookupNo auth

Lookup customer by phone + store context for cashier flow.

Request Body

{
  "phone": "0851234567",
  "storeId": "cuid_store"
}
POST/v1/public/redemptions/startNo auth

Start customer redemption and generate short code.

Request Body

{
  "deviceId": "device-uuid",
  "tenantId": "cuid_tenant",
  "storeId": "optional_store_id",
  "tierId": "optional_tier_id"
}
POST/v1/public/redemptions/regenerateNo auth

Expire active code and issue fresh redemption code.

Request Body

{
  "deviceId": "device-uuid",
  "tenantId": "cuid_tenant",
  "storeId": "optional_store_id"
}
POST/v1/public/cashier-redemptions/redeemNo auth

Cashier directly redeems reward for a customer.

Request Body

{
  "storeId": "cuid_store",
  "customerId": "cuid_customer"
}
POST/v1/public/cashier-redemptions/confirm-codeNo auth

Cashier confirms customer redeem code.

Request Body

{
  "storeId": "cuid_store",
  "redeemCode": "AB12CD34"
}
GET/v1/public/redemptionsX-Device-Id / deviceId

List redemptions for current device and tenant.

Headers

{
  "X-Device-Id": "device-uuid"
}

Query Params

{
  "tenantId": "cuid_tenant",
  "page": 1,
  "limit": 20
}
POST/v1/public/registerNo auth

Register customer with phone/name and optional device link.

Request Body

{
  "tenantId": "cuid_tenant",
  "storeId": "optional_store",
  "name": "Alex",
  "phone": "0851234567",
  "deviceId": "optional_device_uuid"
}
GET/v1/public/register/:storeId/infoNo auth

Registration/cashier store info and earning mode details.

GET/v1/public/card/:customerIdNo auth

Public member card summary.

POST/v1/public/login-phoneNo auth

Link device to phone-based customer account.

Request Body

{
  "phone": "0851234567",
  "tenantId": "cuid_tenant",
  "deviceId": "device-uuid"
}
POST/v1/public/account/deleteNo auth

Anonymize account for device + tenant.

Request Body

{
  "deviceId": "device-uuid",
  "tenantId": "cuid_tenant"
}
POST/v1/public/account/existsNo auth

Check whether account exists for device + tenant.

Request Body

{
  "deviceId": "device-uuid",
  "tenantId": "cuid_tenant"
}

🔁Password Reset

Owner password reset flow endpoints

POST/v1/auth/forgot-passwordNo auth

Request password reset email.

Request Body

{
  "email": "owner@example.com"
}
POST/v1/auth/reset-passwordNo auth

Set new password with reset token.

Request Body

{
  "token": "reset_token",
  "password": "NewStrongPassword123"
}

💻Code Examples

cURL: POS check-in (X-API-Key)bash
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: Owner login (get JWT)bash
curl -X POST https://api.loyaltyplus.app/v1/owner/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "owner@example.com",
    "password": "StrongPass123"
  }'
cURL: Owner list customers (Bearer JWT)bash
curl -X GET "https://api.loyaltyplus.app/v1/owner/customers?page=1&limit=20" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
cURL: Generate/rotate tenant API keybash
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"}'
JavaScript: POS check-in requestjavascript
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();
JavaScript: Owner login + authenticated calljavascript
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();
Python: POS check-in requestpython
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())
JavaScript: Error handling patternjavascript
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
}