Idempotency

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.

How it works

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.

Example — safe retry

safe-retry.ts
// 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-award

Duplicate transaction response

The 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.

Response (200 — cached)
// 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"
}

Best practices

  • Use your POS or order system transaction ID as transactionRef. This guarantees uniqueness without extra coordination.
  • Always retry on network errors (timeouts, 5xx) — never on 4xx client errors.
  • Use exponential backoff with jitter: start at 500ms, cap at 30s, max 5 retries.
  • transactionRef values are scoped per tenant — the same ref is allowed in different tenants.
Important: Idempotency is scoped to 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.
Idempotency — Loyalty Engine Docs