Skip to content

Loyalty Points API Example

A copy-paste walkthrough of the loyalty H2H endpoints: read a customer's balance, grant points after a purchase, redeem points at checkout, and confirm the balance change. Hand this to vendors integrating loyalty with their POS flow.

All three endpoints authenticate with a partner API key (X-Api-Key). The proxy resolves the tenant from your subdomain and forwards the call to the loyalty backend.

Setup

Replace the placeholders with values for your tenant and partner.

export BASE="https://{tenant}.sgp1.samba-technologies.xyz/service/pos-integration"
export KEY="<your-partner-api-key>"
export CUST="<customer-uuid>"

Ask Cata for these values

All three are provisioned by the Cata team — they are not self-serve. Contact your Cata account manager (or email support+api@cata.sg) to request:

Variable What it is How to request
BASE Your tenant's base URL. The {tenant} portion is your tenant subdomain (e.g. acmeacme.sgp1.samba-technologies.xyz). Confirm your tenant subdomain with Cata.
KEY Your partner API key. Long-lived; rotate by asking Cata for a new one. Request per environment (dev / staging / prod).
CUST A test customer UUID for end-to-end validation. Not required in prod — you'll use your own real customerUuid values. Ask Cata for a sandbox customer on your tenant.

For local development, use http://localhost:8080 as $BASE.

Before running the flow, verify your key and tenant routing:

curl -sS "$BASE/api/v1/auth/whoami" -H "X-Api-Key: $KEY" | jq .

A 200 response with "authenticated": true confirms the key is valid for the resolved tenant. If you get 401, the key is missing, invalid, or expired — re-auth before continuing.

1. Check current balance

curl -sS "$BASE/api/v1/loyalty/balance?customerUuid=$CUST" \
  -H "X-Api-Key: $KEY" | jq .
{
  "code": 0,
  "isSuccess": true,
  "message": "OK",
  "balance": 130,
  "numOfPointsWillExpire": 10,
  "nearestExpiryDateTimeUTC": "2026-12-31T15:59:59Z"
}
Field Meaning
balance Current usable points.
numOfPointsWillExpire Points at the nearest upcoming expiry date (0 if no upcoming expiry).
nearestExpiryDateTimeUTC UTC timestamp of that nearest expiry (empty string if none).

2. Grant points (earn)

Typically called when a customer completes a purchase. refCode is your idempotency key — use something unique to the order (e.g. the order ID). If you replay the same refCode, the loyalty service recognises it and does not double-credit.

curl -sS -X POST "$BASE/api/v1/loyalty/earn-points" \
  -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d "{
    \"customerUuid\": \"$CUST\",
    \"points\": 25,
    \"refCode\": \"order-12345-earn\"
  }" | jq .
{
  "code": 0,
  "isSuccess": true,
  "message": "grant_points_success"
}

Other fields you can pass:

Field Required Notes
customerUuid Yes Customer's UUID in loyalty.
points Yes Positive integer.
refCode Yes Your idempotency key.
displayNotes No Customer-facing note.
internalNotes No Internal audit note.
expiryDatetime No Explicit expiry (UTC, YYYY-MM-DD HH:MM:SS). When omitted, loyalty computes the expiry from the tenant's program config.

A replayed refCode returns grant_points_already_processed instead of grant_points_success — that's expected and safe.

3. Redeem points (use)

Called when a customer spends points (e.g. at checkout). FIFO batch consumption — the oldest points are used first.

curl -sS -X POST "$BASE/api/v1/loyalty/use-points" \
  -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d "{
    \"customerUuid\": \"$CUST\",
    \"points\": 10,
    \"refCode\": \"order-12345-redeem\"
  }" | jq .
{
  "code": 0,
  "isSuccess": true,
  "message": "use_points_success"
}

refCode is idempotent here too — a replay returns a safe "already processed" acknowledgement without a second debit.

4. Verify the balance change

After earning 25 and redeeming 10, the balance should move by +15.

curl -sS "$BASE/api/v1/loyalty/balance?customerUuid=$CUST" \
  -H "X-Api-Key: $KEY" | jq .
{
  "code": 0,
  "isSuccess": true,
  "message": "OK",
  "balance": 145,
  "numOfPointsWillExpire": 25,
  "nearestExpiryDateTimeUTC": "2026-12-31T15:59:59Z"
}

Starting balance was 130 → +25 earned → −10 redeemed → 145. The numOfPointsWillExpire reflects points expiring at the nearest upcoming expiry date.

Error handling

Always check isSuccess, not just the HTTP status

The loyalty service returns HTTP 200 for business errors (e.g. insufficient balance, unknown customer). The failure signal lives in the response body: isSuccess: false with a non-zero code and a descriptive message. Treat a 200 with isSuccess: false as a failed operation — do not assume success on HTTP 2xx alone.

The proxy layer's client-side validation (missing fields, non-positive points) still returns HTTP 4xx with the standard error envelope.

Insufficient balance → 200 with code: 1012

Redeeming more points than the customer has available.

curl -sS -i -X POST "$BASE/api/v1/loyalty/use-points" \
  -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d "{
    \"customerUuid\": \"$CUST\",
    \"points\": 99999,
    \"refCode\": \"redeem-999\"
  }"
HTTP/1.1 200 OK
Content-Type: application/json
{
  "code": 1012,
  "isSuccess": false,
  "message": "insufficient_points"
}

No points are deducted. The customer's balance is unchanged. Safe to retry with a lower points value (use a new refCode — reusing the rejected one is not guaranteed to re-run).

Non-positive points400

curl -sS -i -X POST "$BASE/api/v1/loyalty/use-points" \
  -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"customerUuid\":\"$CUST\",\"points\":0,\"refCode\":\"bad\"}"
HTTP/1.1 400 Bad Request
{
  "error": {
    "code": 400,
    "message": "invalid request",
    "details": "points must be greater than 0"
  }
}

Client-side validation — the proxy rejects the request before forwarding to loyalty. Same shape applies to missing customerUuid or missing refCode.

Invalid / missing API key → 401

curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  "$BASE/api/v1/auth/whoami" -H "X-Api-Key: wrong"
HTTP 401

All loyalty endpoints reject missing/invalid/expired keys with 401 and this body:

{
  "error": {
    "code": 401,
    "message": "missing api key",
    "details": "X-Api-Key header is required"
  }
}

Do not retry — re-authenticate.

Missing customerUuid400

curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  "$BASE/api/v1/loyalty/balance" -H "X-Api-Key: $KEY"
HTTP 400
{
  "error": {
    "code": 400,
    "message": "missing required field",
    "details": "customerUuid query parameter is required"
  }
}

Validation errors return 400 without hitting the upstream loyalty service — fix the request and retry.

Other status codes

HTTP When Retry?
400 Body/query validation failed. No — fix input.
401 API key missing / invalid / expired. No — re-auth.
404 Customer not found in this tenant. Depends.
502 Loyalty backend returned an error. Yes, with backoff.
503 Tenant hasn't finished loyalty onboarding. No — tenant admin action needed.
504 Loyalty backend timed out. Yes, with backoff.

Idempotency — at a glance

  • Use a deterministic refCode tied to your own system (order ID, transaction ID, event UUID).
  • Earning and redeeming with the same refCode is safe to replay — the loyalty service returns an acknowledgement without a second effect.
  • Balance reads are naturally idempotent (no refCode).

That's the full loyalty round-trip. Reach out if you hit a response shape that isn't documented here.

See also