Letting a guest spend loyalty points like cash at checkout sounds like a UI feature. It's actually a concurrency problem, solved with database holds, not subtraction.

A loyalty program for hotels that lets members "pay" with points sounds like a UI feature: show a balance, let the guest apply some of it, subtract the points, done. That's roughly how most hotel rewards program building blocks get built, and it works fine right up until two requests hit the same balance at once. A guest taps redeem twice because a kiosk screen lagged for half a second. A booking engine automatically retries a request that timed out. If "spend the points" is a single subtraction, both requests can succeed, and a member has just spent points they no longer had by the time the second one landed.
That isn't an edge case worth hand-waving away. It's the default behavior of any system that treats redemption as one UPDATE statement. It's also why cash credit is built the way it is below, as a lifecycle with a database enforcing it, not a discount field trusting the client.
A member doesn't spend points in one place, so cash credit isn't one feature, it's three, each independently toggleable. A guest can redeem points as real money on a direct booking through the hotel's own booking engine, mid-stay through the guest WiFi portal, or at checkout through a front-desk terminal or kiosk. A hotel can turn on all three, or just one, mid-stay redemption on the WiFi portal without ever touching the booking flow, for instance.
That independence matters because the three surfaces don't carry the same risk profile. A guest redeeming from a phone mid-stay, a browser tab abandoned partway through a booking-engine checkout, a kiosk screen that hangs while a member walks away, each of those is a different way for a redemption to go uncompleted. Those aren't the same transaction wearing three different skins. They need different timing and different failure handling, which is a large part of why the lifecycle underneath all three doesn't collapse into a single "subtract now" call.
A discount field reads a balance, checks it's enough, and writes a smaller number back. That's fine for a value nobody else can touch between the read and the write. A loyalty balance fails that test constantly: a member's phone drops signal mid-checkout and the app retries, or a kiosk POST hangs and the terminal fires it again on timeout. Now two requests read the same starting balance and both believe they're authorized to spend against it.
A subtraction has no memory of having already run. A hold does.
The lifecycle is Quote, then Hold, then one of Confirm, Release, or Expire. Never a single "subtract the points" step. A quote just shows the member what a redemption would look like, no balance is touched at all. A hold is the commitment: it reserves the points against that member's balance so nobody else can spend them in the meantime, but a hold on its own hasn't actually spent anything. Only a confirm does that.
The integration supplies a stable external reference for a hold, so a repeat request can be recognized instead of creating another reservation against the same points. Reusing that reference is part of the integration contract; a new reference represents a different request. If a guest abandons the flow after the hold but before confirming, the points remain reserved until the hold is released or expires, rather than being spent by the hold itself.
None of this works if application code is still the thing doing the arithmetic. It isn't. The balance math runs entirely inside atomic SQL functions on the database: create_loyalty_credit_hold, confirm_loyalty_credit_hold, and a loyalty_member_spendable() function that computes what a member can actually still spend right now, accounting for anything already on hold. Application code calls these functions, it never directly subtracts a balance itself.
The hold flow checks the member's spendable balance and creates the hold under a database lock, instead of separating the check and write in application code. Concurrent requests are evaluated against the points that remain available after existing active holds. A request is rejected if it asks for more than that available balance; two requests can both succeed when there are enough points to cover both.
Unconfirmed holds have an expiry time so an abandoned session does not reserve points indefinitely. The current default hold period is 15 minutes, configurable by the hotel group's cash-credit policy. Once an active hold passes its expiry time, its points no longer reduce the spendable balance; background processing also marks overdue holds as expired. The integration should use the expiry time returned with the hold rather than assume a fixed window for a booking engine or kiosk.
Cash credit only applies to reservations the hotel can actually see the margin on. Bookings sourced from Booking.com, Expedia, and similar OTA or tour-operator channels are hard-blocked from ever using cash credit, and that block holds regardless of how a tenant configures the rest of the feature. There's no setting that opens it up for those channels, because there's no version of "let a guest redeem points against an OTA-sourced booking" that a hotel can price safely without knowing what it actually nets on that reservation. Points as payment for a hotel booking only makes sense where the hotel is the one setting the economics.
A hotel doesn't have to trust the integration blind. Before enabling redemption for real guests, cash credit can run in a live preview mode, a quote-only mode that shows exactly what a real redemption would look like without ever actually creating a hold. That's a way to sanity-check the integration against the real thing, without any risk of a stray hold sitting on a real member's account.
Once enabled, five HMAC-signed webhook event types support updates for holds, confirmations, releases, reversals and balance changes. An expired hold is reported as a release with an expiry reason. Delivery includes retries, so receiving systems should verify signatures and handle repeated notifications rather than assume each notification arrives immediately or only once. The fallback redemption rate is 100 points to 1 unit of currency; the hotel group's configured rate takes precedence.
None of this is complicated to explain to a guest. What's underneath it is the part worth getting right before the feature ever ships, because a loyalty balance is the one part of a loyalty program a member actually checks the math on. If you're still deciding what a program like this should even look like before you get to the payment mechanics, how to launch a hotel loyalty program is the place to start.
Thirty minutes, your real properties, no generic demo environment.