

A cardholder taps their card at a coffee counter. The terminal sends the purchase to the loyalty system, waits, and hears nothing back. A few seconds later, it sends the purchase again.
Somewhere between those two messages, the first one did arrive. The points were awarded. The acknowledgement was simply lost on the way back. Now the same purchase is knocking on the door a second time, and the loyalty system has to decide whether this is a new cup of coffee or the same one.
Getting that decision right is one of the least visible jobs in loyalty engineering, and one of the most important. This article walks through how it is done, what it costs, and why it matters well beyond the engineering team.
A retry is not a mistake. It is the network doing its job.
When a terminal sends a request and hears nothing back, it cannot tell the difference between “the request never arrived” and “the request arrived, but the answer got lost.” From where it sits, the two look identical.
Resending is the sensible choice. Giving up would risk a guest missing points they earned, and in payments a silent failure is usually worse than a noisy one. So terminals, gateways, and apps are built to retry. That means the question is never whether duplicates will reach the loyalty system. It is what the system does when they arrive.
The fix is a promise: same request, same answer.
The technical name for that promise is idempotency — an operation can be repeated any number of times and still have the effect of doing it once. Pressing an elevator call button five times does not summon five elevators. That is idempotency.
The web’s own rules already draw this line. RFC 9110, the standard that defines HTTP, classes some request types as idempotent and others — including the POST requests most systems use to record a transaction — as not. Awarding points is exactly the kind of action that has to be made idempotent on purpose, because it will not be by default.
The usual way to do it is an idempotency key: a unique label attached to each purchase. The first time the loyalty system sees a key, it awards the points and stores the result alongside it. Every later request carrying the same key gets that stored result back, and nothing new is written. The guest earns once. The terminal gets its answer. Nobody has to reconcile anything by hand.
The key has to come from the purchase, not from the attempt.
This is where good intentions most often come apart. If the component that sends the request creates a fresh key every time it tries, then each retry looks brand new, and the protection disappears without anyone noticing. Every test with a single request passes. The duplicates only show up in production, under real network conditions, weeks later.
The key needs to be built from something that identifies the purchase itself — such as the terminal and the transaction reference — so every attempt at the same purchase carries the same label. That makes idempotency a shared discipline across every system in the path, not a feature that lives in one of them.
Remembering has a cost, and it is worth naming.
An idempotency key is only useful while it is remembered. Keep it for too short a time and a late retry, one that sat in a queue during an outage, slips through as a new purchase. Keep it forever and the store of keys grows without limit. The right retention window is a business decision as much as a technical one: long enough to cover the latest point at which a retry could realistically arrive.
There is also a timing subtlety. If two copies of the same request arrive at almost the same instant, both can check for the key, both can find nothing, and both can award points. The check and the write have to happen as one indivisible step, typically enforced by the database itself refusing a second record with the same key. It is a small detail that separates a design that works in a demo from one that works on a busy Saturday.
Redemption raises the stakes.
Earning twice is generous. Spending twice is not. When a guest redeems points for a reward and the request is retried, a system without idempotency can deduct the points twice, or issue two rewards for one balance. The first leaves a guest short and unhappy; the second quietly gives value away.
The same logic applies to reversals. A refund that is retried should reverse the original earn once, not twice. Every movement of value, in either direction, benefits from the same promise.
Why this belongs on a CFO’s radar.
Outstanding points are not a marketing metric. They represent value the business has promised to its cardholders and guests, and finance teams track them as such. Every duplicate earn inflates that obligation by a small amount, and those amounts do not announce themselves. They show up later as balances that no longer reconcile against sales, and as time spent explaining the difference.
Idempotency is what lets the number of purchases and the number of rewards agree without anyone having to check.
A simple test for any loyalty program.
- Send the same purchase twice, on purpose, a few seconds apart.
- Send it twice at exactly the same moment.
- Send it again an hour later.
If the balance moves once in every case, the system is keeping its promise. If it moves more than once in any of them, the program is paying for purchases that only happened once.
How PayCloud approaches it
Our SaaS Loyalty Platform treats every earn, redemption, and reversal as a keyed, atomic operation — so loyalty stays part of the credential, not a separate ledger that drifts from sales.






