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. acme → acme.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 points → 400¶
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 customerUuid → 400¶
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
refCodetied to your own system (order ID, transaction ID, event UUID). - Earning and redeeming with the same
refCodeis 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¶
- Full API reference (all partner endpoints, schemas, examples): https://apidocs.cata.sg/