Network failures happen. The Loyalty Engine is designed to be safe to retry — retrying a failed request will never double-award points, issue a duplicate reward, or double-redeem a balance.
Every POST /evaluate and POST /redeem request requires a transactionRef — a unique string you generate (typically your POS transaction ID or order number).
If you submit the same transactionRef twice, the second request returns the original response with no side effects. Points are not re-awarded. The response includes an idempotent: true field to indicate the result was served from cache.
// Safe to call multiple times — only awards points once
const body = {
transactionRef: 'txn_pos_12345', // ← your POS transaction ID
memberRef: 'customer_27821234567',
amount: 15000,
};
// First call — awards 150 points, returns 200
const first = await fetch('/tenants/TENANT_ID/transactions/evaluate', {
method: 'POST',
headers: { 'X-API-Key': 'sk_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
// Network timeout — safe to retry with the same body
const retry = await fetch('/tenants/TENANT_ID/transactions/evaluate', {
method: 'POST',
headers: { 'X-API-Key': 'sk_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
// retry response === first response — same data, no double-awardThe engine returns 200 OK with the original result — no special status code, no extra field. Your code does not need to handle duplicates differently; treat the response the same as a first-time call.
// The engine returns the same 200 response — no extra field flags.
// Points are NOT re-awarded. The response is identical to the first call.
{
"transactionId": "d2f3a1b4-...",
"transactionRef": "txn_pos_12345",
"memberId": "9f4c2e8a-...",
"memberCreated": false,
"pointsAwarded": 150,
"pointsBalance": 650,
"campaigns": [
{
"campaignId": "5a7b3c9d-...",
"campaignName": "Spend & Earn",
"pointsEarned": 150,
"rewardIds": []
}
],
"state": "PROCESSED"
}transactionRef. This guarantees uniqueness without extra coordination.transactionRef values are scoped per tenant — the same ref is allowed in different tenants.transactionRef + tenantId. Using the same ref with different body parameters (e.g. different basket total) will still return the original result — the engine does not re-evaluate on body mismatch.