A referral bonus and a milestone bonus can both fire on a guest's first stay. Here's the real bug we caught, and how hotel referral programs actually pay out.

"First stay" sounds like the simplest phrase in loyalty program logic. A guest books, arrives, checks out, and that's it: the qualifying event has happened. Until a hotel group turns on more than one way to earn points, and two of those ways can legitimately fire on the exact same reservation. Then "first stay" stops being a fact about a reservation and starts being a question about which bonus gets to claim it.
That question showed up while building referral and non-stay earning for GuestMaker's loyalty program, and it's a good way to explain how both actually work, because the fix says more about the mechanism than a feature list would.
Every enrolled member gets their own referral link, a stable URL shaped like /loyalty/<program>/join?ref=<code>, that they can hand to a friend, post online, or text before a trip. Nothing unusual there. The part that matters is when the payout fires.
A referral bonus pays both the referrer and the new member, but not on sign-up. It pays once the referred friend completes a qualifying stay or a qualifying spend. A hotel group could give out a bonus the moment someone clicks a link and creates an account, but that rewards intent, not a real guest. Gating the payout on a completed stay means the referral program is actually paying for bookings, not for link clicks.
Under the hood, that payout runs through the same idempotent points-earning code path a normal booking-driven point earn already uses. That's not a small detail. It's the same mechanism that already keeps a normal booking-driven point earn from being paid twice, reused rather than rebuilt for referral. The referral bonus isn't a parallel system bolted onto loyalty. It's the same earning engine, told to fire under a different trigger.
Referral is one way to earn without a fresh booking. It's not the only one. Members can also earn a fixed, one-time bonus for actions that have nothing to do with booking a stay at all: leaving a review, completing a survey, finishing their profile, downloading the hotel's app, or following the hotel on social media.
Each of those fires once per member, ever. A guest who reviews three separate stays doesn't get three review bonuses. The first one is the only one that counts. That's a deliberate constraint, and it's what makes non-stay earning safe to turn on in the first place: a fixed one-time bonus for an action can't be farmed, because a second attempt at the same action simply doesn't earn anything.
This is what a loyalty program without booking as the only trigger actually looks like in practice. A member who never travels but still downloads the app, follows the hotel's account, or fills out a survey earns something for it, once, without a reservation anywhere in the picture. It's a small mechanism, but it changes what the loyalty program is measuring: not just stays, but engagement with the brand between stays.
Put those two mechanisms side by side, referral and non-stay earning, and they look like clean, independent features. They're not, quite. They share a guest, and sometimes they share a moment.
Here's the scenario that surfaced the bug before it ever shipped. A friend uses a referral link to join a hotel's loyalty program, books a room, and arrives for their first-ever stay. During that same stay, they also do something that earns a non-stay bonus: they finish their profile, or they leave a review before checkout. Two bonuses are now trying to fire off the same reservation. One is the referral payout, supposed to trigger the moment this stay counts as the referred friend's qualifying stay. The other is a milestone or non-stay bonus, credited during that stay for an unrelated action.
The risk was in how "has the qualifying stay happened yet" got checked. If that check simply asked whether the member had already earned any stay-linked bonus, it would see the profile-completion or milestone bonus credited during that first stay and read it as proof the qualifying event had already occurred, just from the wrong source. The referral system would conclude its job was done and skip the payout, because as far as it could tell, the first stay had already been "used" by another bonus. The referrer, and the new member, would simply never get paid, on the exact stay the whole referral program was built to reward.
Nothing about that would show up as an error. No failed request, no exception, no support ticket that points at the right line of code. It would just look like a referral that quietly never paid out, on the one stay where it mattered most: the very first one.
The fix wasn't a one-off patch for this scenario. It was an explicit exclusion list of earn sources, referral bonuses, milestone bonuses, birthday bonuses, and similar non-stay earns, that are never allowed to count as the qualifying stay for a referral payout. Whatever check decides "has this referred member's qualifying stay happened" now has to find a stay-based earn outside that list, not just any earn event that happens to be timestamped inside a stay.
That's a more durable fix than special-casing "profile completion during a first stay," because the same collision isn't limited to that one combination. A birthday bonus could land during a first stay just as easily as a milestone bonus could. A hotel group that adds a new non-stay action later shouldn't have to remember to re-solve this problem. The exclusion list means any future non-stay earn source is automatically kept out of the referral qualification check, by category, not by name.
A referral program that pays out real points invites an obvious abuse case: a member using their own referral link to pay themselves. That gets checked in two separate places, not one, rather than trusting a single check to catch every way a member could reach the qualifying event. Two independent checks mean there isn't a single point of failure for a self-referral to slip through, and it's the same instinct behind the exclusion list above: don't trust one place in the code to catch every way the same outcome can be reached.
None of this runs by default. Referral is off. Non-stay action bonuses are off. Even notifying a referrer that their friend has joined, a much lower-stakes feature than the payout itself, is off. A hotel group has to explicitly turn on each one.
That's worth sitting with for a second, because it's the opposite of how a lot of generic hotel rewards program add-ons get shipped: enabled everywhere, configurable if you go looking. Referral and non-stay earning touch real points, real payouts, and a qualifying-stay check that, as the bug above shows, has more edge cases than the phrase "first stay" suggests. Defaulting every piece to off means a hotel group adopts referral because it decided to run a referral program, not because a toggle somewhere defaulted to on and nobody noticed.
If a hotel group is still deciding whether a loyalty program is worth building at all, that's a separate, earlier question, one covered in how to launch a hotel loyalty program. Referral and non-stay earning are additions to a program that already exists, not a starting point. But the same principle holds at both stages: the parts of a loyalty program that touch money need to be built assuming two things will eventually happen on the same reservation, because eventually, they will.
Twenty minutes, your real properties, no generic demo environment.