Loyalty API Reference

Complete REST API documentation for customer loyalty, points, vouchers, and rewards management. Integrate loyalty functionality into your POS, mobile app, or e-commerce platform.

Introduction

The KiriMel Loyalty API allows you to programmatically manage customer loyalty programs, including registration, points earning/redemption, voucher management, and rewards. Built for seamless integration with POS systems like VectorPOS (VectorDine and VectorRetail).

๐Ÿš€ Quick Start

Get started in 3 steps: 1) Create a loyalty group, 2) Add your sites, 3) Register customers and start awarding points. See the endpoints below for details.

โš ๏ธ API Key Required

All Loyalty API endpoints require an API key with HMAC authentication. Generate your API keys in the Loyalty Dashboard after creating your loyalty group.

๐Ÿ“– Workflow Guides Available

Need step-by-step integration examples? Check out our Loyalty API Workflows page for complete POS integration scenarios including member checkout, voucher redemption, and points payment.

All Endpoints

Method Endpoint Purpose
POST/group/createCreate a loyalty group
POST/group/sites/addAdd a site/outlet to the group
POST/customers/registerRegister a new loyalty member
GET/customers/lookupLookup member by phone/QR/code
GET/customers/lookup-by-emailLookup member by email
PUT/customers/{id}Update name/birthdate (phone/email need OTP)
POST/customers/{id}/identity-change/requestRequest OTP for phone/email change
POST/customers/{id}/identity-change/confirmConfirm OTP and apply the change
POST/points/earnAward points for a transaction
GET/points/redeem-configRedemption rules for POS UIs
POST/points/preview-redeemValidate redemption without committing
POST/points/commit-redeemDeduct points for a transaction
POST/points/reverseVoid/refund all movements for a reference
GET/wallet/balanceGet member's current point balance
POST/vouchers/batchesCreate a voucher batch
POST/vouchers/redeemRedeem a voucher by code

Authentication

The Loyalty API uses HMAC-SHA256 signature authentication for security. Each request must include three headers:

X-API-Key: your_api_key_here
X-Timestamp: 2024-01-01T12:00:00Z
X-Signature: computed_hmac_signature_here

Signature Calculation

The signature is computed over the timestamp and request body joined by a period (.):

signature = HMAC-SHA256(timestamp + "." + requestBody, apiSecret)

The timestamp is ISO format (e.g., 2024-01-01T12:00:00Z) and must be within ยฑ5 minutes of server time. For GET requests, use {} as the request body.

PHP Example

$timestamp = gmdate('Y-m-d\TH:i:s\Z');
$signature = hash_hmac('sha256', $timestamp . '.' . $requestBody, $apiSecret);

$headers = [
    'X-API-Key: ' . $apiKey,
    'X-Timestamp: ' . $timestamp,
    'X-Signature: ' . $signature
];

Node.js Example

const crypto = require('crypto');
const timestamp = new Date().toISOString();
const signature = crypto.createHmac('sha256', apiSecret)
    .update(timestamp + '.' + requestBody)
    .digest('hex');

Python Example

import hmac
import hashlib
import time

timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
signing_string = (timestamp + '.' + body).encode()
signature = hmac.new(apiSecret.encode(), signing_string, hashlib.sha256).hexdigest()

Rate Limits

API requests are rate-limited per API key to ensure fair usage:

  • Default: 100 requests per minute
  • Burst: 200 requests per minute (for short spikes)
  • Enterprise: Unlimited (contact sales for details)

Rate limit headers are included in every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200

Create Loyalty Group

Creates a new loyalty group (company) with an associated tenant database. Each merchant can have one loyalty group containing multiple sites.

POST /group/create

Request Body

Parameter Type Required Description
company_slug string Required URL-safe company identifier (lowercase letters and numbers only, min 3 chars)
company_name string Required Legal company name
business_type string Required RESTAURANT or RETAIL

Example Request

curl -X POST http://localhost/api/loyalty/group/create \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d "company_slug=myrestaurant&company_name=My+Restaurant+Sdn+Bhd&business_type=RESTAURANT"

Response

{
  "success": true,
  "group_id": 123,
  "group_code": "myrestaurant_a1b2c3d4",
  "tenant_db": "kiriloyalty_myrestaurant_a1b2c3d4"
}

Add Site to Group

Add a new location/site to an existing loyalty group. A loyalty group can have multiple sites (e.g., main restaurant, branch outlet).

POST /group/sites/add

Request Body

Parameter Type Required Description
site_code string Required Unique site code (uppercase letters and numbers only, min 3 chars)
site_name string Required Display name for the location
site_type string Required RESTAURANT or RETAIL
address string Optional Street address
city string Optional City
state string Optional State/province
postal_code string Optional Postal code
phone string Optional Contact phone (include country code)

Example Request

curl -X POST http://localhost/api/loyalty/group/sites/add \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d "site_code=MAIN01&site_name=Main+Restaurant&site_type=RESTAURANT&address=123+Main+St&city=Kuala+Lumpur&phone=%2B60123456789"

Response

{
  "success": true,
  "site_id": 456,
  "site_code": "MAIN01"
}

Register Customer

Register a new customer in the loyalty program. Customers can be identified by phone number or QR code.

POST /customers/register

Request Body

Parameter Type Required Description
phone string Required Phone number with country code (e.g., +60123456789)
name string Optional Customer name
email string Optional Email address (also adds the customer to the merchant's email audience when provided)
birthdate string Optional Birthday for birthday rewards (YYYY-MM-DD format)

Example Request

curl -X POST http://localhost/api/loyalty/customers/register \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d '{"phone": "+60123456789", "name": "John Doe", "email": "john@example.com", "birthdate": "1990-05-15"}'

Response

{
  "success": true,
  "data": {
    "id": 123,
    "site_id": 456,
    "phone": "+60123456789",
    "name": "John Doe",
    "email": "john@example.com",
    "status": "ACTIVE",
    "balance_points": 0,
    "balance_version": 1,
    "created_at": "2026-05-11T10:00:00Z"
  }
}

Lookup Customer

Find an existing customer by phone number, QR code, or promo code. Returns customer profile with current points balance.

GET /customers/lookup

Query Parameters

Parameter Type Required Description
phone string Conditional* Phone number with country code
identifier string Conditional* QR code or promo code

*At least one parameter must be provided

Example Request

curl -X GET "http://localhost/api/loyalty/customers/lookup?phone=%2B60123456789" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here"

Response

{
  "success": true,
  "data": {
    "id": 123,
    "site_id": 456,
    "phone": "+60123456789",
    "name": "John Doe",
    "email": "john@example.com",
    "status": "ACTIVE",
    "balance_points": 1500,
    "joined_at": "2026-05-01T10:00:00Z",
    "last_earn_at": "2026-05-10T12:00:00Z",
    "last_redeem_at": "2026-05-09T15:30:00Z",
    "last_seen_at": "2026-05-10T14:00:00Z"
  }
}

Lookup Customer by Email

Find an existing customer by email address. Returns customer profile with current points balance.

GET /customers/lookup-by-email

Query Parameters

Parameter Type Required Description
email string Required Email address to lookup

Example Request

curl -X GET "http://localhost/api/loyalty/customers/lookup-by-email?email=john%40example.com" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here"

Response

{
  "success": true,
  "data": {
    "id": 123,
    "site_id": 456,
    "phone": "+60123456789",
    "name": "John Doe",
    "email": "john@example.com",
    "status": "ACTIVE",
    "balance_points": 1500,
    "joined_at": "2026-05-01T10:00:00Z",
    "last_earn_at": "2026-05-10T12:00:00Z",
    "last_redeem_at": "2026-05-09T15:30:00Z",
    "last_seen_at": "2026-05-10T14:00:00Z"
  }
}

Earn Points

Award points to a customer after a purchase. A wallet is created automatically for wallet-less members on first earn.

POST /points/earn

Request Body

Parameter Type Required Description
customer_id integer Required Customer ID from registration or lookup
points integer Required Points to award (must be positive)
reference_type string Required Reference type, e.g., TRANSACTION, VOUCHER
reference_id string Required POS-side unique reference (receipt/transaction ID)
notes string Optional Human-readable note โ€” use the receipt number here (e.g., Sale #R-00123); shown in the merchant's transaction history

๐Ÿ” Automatic Deduplication

The tuple (customer_id, transaction_type, reference_type, reference_id) is unique at the storage layer. If your POS retries an earn with the same reference, KiriMel returns the original ledger entry โ€” no double credit. Always send a stable receipt/transaction ID as reference_id.

Example Request

curl -X POST http://localhost/api/loyalty/points/earn \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d '{"customer_id": 123, "points": 50, "reference_type": "TRANSACTION", "reference_id": "TXN-2026-0829-00123", "notes": "Sale #R-00123"}'

Response

{
  "success": true,
  "data": {
    "id": 456,
    "customer_id": 123,
    "transaction_type": "EARN",
    "points_delta": 50,
    "balance_after": 983,
    "reference_type": "TRANSACTION",
    "reference_id": "TXN-2026-0829-00123",
    "notes": "Sale #R-00123",
    "metadata": null,
    "created_at": "2026-08-29T10:00:00Z"
  }
}

Get Redemption Config

Redemption rules for building POS redemption UIs. Read once at terminal startup or settings sync.

GET /points/redeem-config

Response

{
  "success": true,
  "data": {
    "earn_conversion_value": 1.0,
    "redeem_conversion_value": 0.01,
    "min_redeem_points": 100,
    "max_redeem_percent": 100
  }
}

Redemption math: max_points = min(balance, floor(bill_amount ร— max_redeem_percent / 100 รท redeem_conversion_value))

Preview Redemption

Validate a redemption against the customer's balance and your redemption rules without committing. Always preview before committing at the till.

POST /points/preview-redeem

Request Body

Parameter Type Required Description
customer_id integer Required Customer ID
amount decimal Required Bill amount (must be positive)
requested_points integer Required Points the customer wants to redeem

Response (200 OK)

{
  "success": true,
  "data": {
    "success": true,
    "points": 500,
    "currency_value": 5.00,
    "balance": 983,
    "balance_after": 483,
    "min_points": 100,
    "max_points": 5000
  }
}

Response (400 โ€” rule violation)

Failures carry the bounds so your UI can show "min 100 pts / max RM 50.00" without a second round-trip:

{
  "success": false,
  "error": {
    "message": "Exceeds maximum redeemable points",
    "preview": {
      "success": false,
      "error": "Exceeds maximum redeemable points",
      "max_points": 2500,
      "balance": 983
    }
  }
}

Commit Redemption

Deduct points for a transaction. Only call this after a successful preview. Same deduplication semantics as earn โ€” a retried commit with the same reference returns the original ledger entry.

POST /points/commit-redeem

Request Body

Parameter Type Required Description
customer_id integer Required Customer ID
points integer Required Points to redeem (must be positive)
reference_type string Required e.g., TRANSACTION
reference_id string Required POS-side unique reference
notes string Optional e.g., Redeemed RM5.00 off Sale #R-00124

Response (201 Created)

Ledger entry with transaction_type: "REDEEM" and negative points_delta (same shape as the earn response).

Reverse Transaction

Void/refund all point movements for a reference (e.g., a refunded sale). Aggregates all EARN/REDEEM movements for that reference into a single REVERSE ledger row per customer. Idempotent โ€” re-reversing returns the existing REVERSE entry. Returns 404 if no earn/redeem entries exist for the reference.

POST /points/reverse

Request Body

Parameter Type Required Description
original_reference_type string Required Reference type of the original transaction
original_reference_id string Required Reference ID of the original transaction
notes string Optional e.g., Transaction voided at register 2

Response (201 Created)

{
  "success": true,
  "data": {
    "id": 458,
    "customer_id": 123,
    "transaction_type": "REVERSE",
    "points_delta": -50,
    "balance_after": 933,
    "reference_type": "TRANSACTION",
    "reference_id": "TXN-2026-0829-00123",
    "notes": "Transaction voided at register 2",
    "metadata": {
      "original_ledger_ids": [456, 457]
    },
    "created_at": "2026-08-29T11:00:00Z"
  }
}

Update Customer

Update a customer's profile. Fields are tiered by security:

  • name, birthdate โ€” applied immediately
  • phone, email โ€” identity fields, rejected with 403 VERIFICATION_REQUIRED; use the Identity Change Verification endpoints (OTP emailed to the member's email on file)
  • status โ€” not allowed via the POS API (merchant dashboard only)
PUT /customers/{id}

Example Request

curl -X PUT http://localhost/api/loyalty/customers/123 \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d '{"name": "John Smith", "birthdate": "1990-05-15"}'

Response โ€” identity field submitted (403)

{
  "success": false,
  "error": {
    "message": "Identity fields require OTP verification",
    "code": "VERIFICATION_REQUIRED",
    "pending_fields": ["phone"],
    "applied_fields": ["name"],
    "next_step": "POST /api/loyalty/customers/123/identity-change/request"
  }
}

Identity Change Verification (OTP)

Phone and email are the member's account credentials. Changing either requires a 6-digit OTP emailed to the customer's existing email on file โ€” the change applies only after confirmation. Rules: 10-minute expiry, max 5 wrong attempts (then VERIFICATION_LOCKED), max 3 requests per customer per hour, one active verification per customer+field. Duplicate values are rejected (409); members with no email on file return 422 IDENTITY_NO_EMAIL (merchant admin override required).

POST /customers/{id}/identity-change/request

Request Body

Parameter Type Required Description
field string Required phone or email
new_value string Required The requested new value

Response (200 OK)

{
  "success": true,
  "data": {
    "verification_id": 42,
    "verification_token": "b7cb41bbf6d065559a405952b1372b65",
    "field": "phone",
    "expires_at": "2026-08-29 03:04:16",
    "masked_email": "j***n@example.com"
  }
}
POST /customers/{id}/identity-change/confirm

Request Body

Parameter Type Required Description
verification_token string Required Token from the request response
code string Required 6-digit code from the member's email

Response (200 OK)

The updated customer object (same shape as lookup). Errors: INVALID_CODE 401 (attempts remaining in message), VERIFICATION_EXPIRED 410, VERIFICATION_LOCKED 429, DUPLICATE_VALUE 409.

๐Ÿงพ POS Flow

Attempt PUT /customers/{id} first. On 403 VERIFICATION_REQUIRED, ask the member for the 6-digit code from their email, then call request + confirm.

Get Customer Balance

Retrieve a customer's current points balance.

GET /wallet/balance

Query Parameters

Parameter Type Required Description
customer_id integer Required Customer ID from registration or lookup

Example Request

curl -X GET "http://localhost/api/loyalty/wallet/balance?customer_id=123" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here"

Response

{
  "success": true,
  "data": {
    "balance": 983
  }
}
POST /wallet/recalculate

Repair operation: rebuilds the wallet balance from the ledger sum. Request body: {"customer_id": 123}. Response: {"success": true, "data": {"balance": 983, "recalculated": true}}

Create Voucher Batch

Generate a batch of unique vouchers for distribution. Each voucher has a unique 12-character code.

POST /vouchers/batches/create

Request Body

Parameter Type Required Description
name string Required Batch name (e.g., "Grand Opening Promo")
type string Required Voucher type: PERCENT, FIXED, or BOGO
value decimal Required Voucher value (percentage or amount)
quantity integer Required Number of vouchers to generate (max 1000 per batch)
valid_from string Optional Valid from date (YYYY-MM-DD format)
valid_until string Optional Valid until date (YYYY-MM-DD format)
min_purchase decimal Optional Minimum purchase amount for voucher use

Example Request

curl -X POST http://localhost/api/loyalty/vouchers/batches/create \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d "name=Grand+Opening+Promo&type=PERCENT&value=10&quantity=100&valid_from=2024-06-01&valid_until=2024-12-31&min_purchase=50"

Response

{
  "success": true,
  "batch_id": "BATCH_001",
  "vouchers_created": 100,
  "voucher_codes": ["VOUCHER_A1B2C3D4E5F6", "VOUCHER_G7H8I9J0K1L2", ...]
}

Redeem Voucher

Redeem a voucher code, applying the discount or reward to the customer's transaction. Validates voucher expiry and usage status.

POST /vouchers/redeem

Request Body

Parameter Type Required Description
code string Required Voucher code (12-character base62 string)
customer_id string Required Customer ID redeeming the voucher
purchase_amount decimal Optional Purchase amount for validation (for vouchers with min_purchase)
reference_id string Optional External reference ID (e.g., order ID, transaction ID)

Example Request

curl -X POST http://localhost/api/loyalty/vouchers/redeem \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-API-Key: your_api_key" \
  -H "X-Timestamp: 2024-01-01T12:00:00Z" \
  -H "X-Signature: signature_here" \
  -d "code=VOUCHER_A1B2C3D4E5F6&customer_id=CUST_123456&purchase_amount=75.00&reference_id=ORDER_456"

Response

{
  "success": true,
  "redemption_id": "REDEEM_123",
  "voucher_type": "PERCENT",
  "voucher_value": 10,
  "discount_applied": 7.50,
  "new_purchase_amount": 67.50
}

Error Handling

The API uses standard HTTP response codes and returns detailed error information in JSON format.

Error Response Format

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Customer does not have enough points to complete this redemption",
    "details": {
      "requested": 100,
      "available": 50,
      "shortfall": 50
    }
  }
}

HTTP Status Codes

Code Description
200 Request successful
400 Bad Request - Invalid parameters
401 Unauthorized - Invalid API key or signature
404 Not Found - Resource doesn't exist
429 Too Many Requests - Rate limit exceeded
500 Internal Server Error

Common Error Codes

Error Code Description
INVALID_API_KEY API key is invalid or expired
SIGNATURE_INVALID HMAC signature verification failed
VALIDATION_ERROR Request validation failed (422, with per-field errors)
VERIFICATION_REQUIRED Identity field change (phone/email) requires OTP verification (403)
INVALID_CODE Wrong OTP code โ€” attempts remaining in the message (401)
VERIFICATION_EXPIRED OTP code expired or already used (410)
VERIFICATION_LOCKED Too many wrong OTP codes โ€” request a new verification (429)
IDENTITY_NO_EMAIL Member has no email on file; merchant admin override required (422)
DUPLICATE_VALUE Another member already uses this phone/email (409)
CUSTOMER_NOT_FOUND Customer ID does not exist
VOUCHER_EXPIRED Voucher has passed expiry date
VOUCHER_ALREADY_REDEEMED Voucher has already been used
VOUCHER_NOT_FOUND Voucher code does not exist
TOO_MANY_REQUESTS Too many OTP requests or rate limit exceeded (429)
SITE_LIMIT_REACHED Maximum number of sites reached for your plan