Assembling a hotel stay people can trust
I wanted to work through a product problem that's genuinely messy: assembling a multi-room hotel booking from inconsistent supplier data you can't clean up.
The setting is a B2B travel platform. Fintechs and banks embed booking through an SDK, so the traveler is inside a partner app, not a travel brand. If the room or the terms are wrong, they blame the bank. The bank then blames the platform.
The screens below are from the prototype. The rest is why they look that way.
What I took as given
The traveler has already chosen the hotel, the dates, and who is going. This work starts at room selection.
More than one supplier can price the same room. The price on screen is already marked up by the partner. When the tax engine knows the tax, the stay total includes it.
The first version covers 1 to 4 rooms. Groups of 5 or more are out.
Breakfast and refundability show up as real fields on some suppliers and as messy text on others. There is no deep history of past bookings to learn from, and I am not training a model to rewrite room names or invent missing terms.
Mobile first. Some rooms have no photos.
The problem
This is an assembly-and-trust problem, not a display problem. That is why the cleanup of supplier data sits under the screen, not inside a smarter list. Restyling the supplier names would still leave the traveler staring at three versions of the same room.
After the hotel is chosen, they have to put together a set of rooms they trust. They stall because the offers look the same, differ in the wrong places, and the total does not obviously add up. That gets worse with more than one room.
This is not “add filters to a long list.” The hotel is already chosen.
The person doing this is booking inside a partner app, often for a family or a small group. They are not on the platform’s brand.
It is hard because the platform cannot clean supplier descriptions. The same room arrives as three strings, from three suppliers, at three prices, after the partner’s markup. A prettier list that still leads with those names fails. Wrong breakfast or cancel text becomes a support ticket and a chargeback.
It is also hard because partners want different commercial setups. One bank forbids non-refundable rates. Another must show tax included. A third wants a higher-margin rate featured. Same inventory, different rules, one SDK. If those rules are hardcoded into the screen, every embed forks.
I have hit this split before, on Omnix360, while taking in messy school calendars and course packs. The temptation is always to paper over it in the UI. What lasts is a thin, boring mapping layer and an honest empty state.
If this is solved badly, conversion can rise while trust falls. People book the cheapest non-refundable rate they did not understand, or a “Deluxe” that was actually a suite. Support load and partner churn cost more than the extra bookings.
Other companies have already shown both sides of this. Rocket Travel by Agoda grouped rates by room type instead of a flat list and saw a 5% conversion lift among travelers booking to earn or redeem points. Agoda later shipped a Room Grid Bot, a chat on the room page that recommends a “best pick.” I am not doing that here. A hidden “best” is a support problem and a commercial problem. The partner cannot see the rule, and nobody can defend breakfast a chat invented. Booking.com’s partner docs still split a display price on the room page from the real total at checkout. That is the leak I am designing against. The stay total belongs on this screen, before anyone has committed.
How I would build it
A room-selection module for the SDK: a thin normalization layer, plus a mobile basket. The decisions are rules the traveler can see.
| Step | Input | Rule | If the data is bad |
|---|---|---|---|
| Count rooms | Occupancy already collected | Ask how many rooms. Suggest a split. Never force it. | Default to 1. Let them add rooms. |
| Family | Room name, occupancy, bed count, room code | Cluster offers into families. Our label leads. The raw supplier name stays secondary. | Skip clustering. Flat list, still with chips, sorted by stay total. |
| Rate chips | Board code, refund flag, cancel-by date, tax-inclusive price | Show only structured differences: breakfast, refund, price. | Omit the chip. Never guess breakfast or cancel. |
| Same vs mix | Room count | Default every slot to the same rate. “Make this room different” is opt-in. | If a copied rate sells out, keep the family and swap the live rate. |
| Which rate to feature | Price gap, refund flag, partner rules | Do not pre-select non-refundable. Pre-select nothing, or the flexible rate in the open family. | If flexible is missing, show price first and mark cancel as unknown. |
| Total | Partner markup + tax engine | Always the stay total for all rooms, taxes in. Nightly is secondary. | If tax is unknown, say so. Do not write “no extra fees.” |
Three pieces, not one screen.
- Normalize. Map board, refund, occupancy, and cancel-by from supplier payloads. Confidence is binary: show it, or leave it out. Partners reuse the same mapper.
- Compose the price. Supplier price, plus partner markup, plus tax. The module does not invent commercial rules. It shows what pricing already decided.
- One SDK module. Partner skin, currency, max rooms, and “allow non-refundable” are config.
What actually changes the design
| What varies | When it changes the answer | How much it matters |
|---|---|---|
| Can we actually say it? | Unknown breakfast or cancel means hide the claim. Trust beats a complete-looking card. | Most |
| How many rooms | One room is families and rates. Two to four rooms are slots, same setup by default. Five or more waits. | A lot |
| Price spread inside a family | A wide spread leads with price. A tight spread leads with cancel and breakfast. | A lot |
| Partner commercial rules | Some partners forbid non-refundable or require tax-inclusive. That is config, not a forked screen. | A lot |
| Does the room fit the party? | A room that cannot sleep everyone is pushed down, not hidden, and labeled “sleeps 2.” | Some |
| Is there a photo? | A missing photo is a labeled hotel photo. Never a broken card. | Some |
What I am optimizing for is completion with a total the traveler understands, and terms support can defend. Trust-weighted conversion, not raw tap-through.
A partner may want the higher-margin non-refundable rate pushed to the top. The first version shows that rate. It does not default it. If a partner needs that push, it is a later, explicit setting, not a silent sort.
What I chose not to build
The fastest path to the cheapest rate. That is how people book non-refundable by accident, then call the partner two days later. I would rather lose that tap.
A full dump of amenities. Supplier text is not a product. Showing all of it brings back the stall.
Perfect room clustering. Merging a suite into a standard room is worse than a longer list. I am not sure six offers is the right cutoff for “try to cluster, otherwise flatten.” That is a guess I would revisit after the first partner holdout.
Maximum flexibility in the first version. Mix-and-match, connecting rooms, and group booking can wait. Copying the same room into every slot covers most family and small-group trips.
Also out: a recommender, a “best for families” model, floor plans, view maps, rewriting supplier descriptions with a language model, a photo gallery the platform does not have assets for, and a chat bot that picks the room.
The screens
- How many rooms? Occupancy is already known. The screen suggests a split. Two adults and one child can share a family room, or take two rooms. The product does not decide. Two or more rooms default to the same setup. “I need different rooms” is something they opt into.
- Choose rooms. Offers are grouped into families, such as Deluxe King, Superior Twin, and Family. Inside a family, they pick a rate by what actually differs: stay total, refundability, breakfast. The supplier name is small type, never the title.
- Check the total. A line per room, then one stay total, taxes included. Guest details come after they accept the number.
- Missing data. If breakfast or cancel is unconfirmed, the screen says so. They can still book. The product does not invent the terms.
- Sold out. The continue button dies. The family stays. The next live rate is offered.
In front of them: room family, how many it sleeps, beds, stay total, nightly equivalent, refundability, breakfast only when it is known, and a live basket total.
Kept off the screen: supplier identity as a title, raw amenity dumps, “best value” scores, floor, view unless it is a real field, loyalty points, and a one-tap cheapest non-refundable default.
How I would know it worked
I would split one-room trips from trips with two or more rooms. If only the one-room path improves, this shipped a prettier list.
Primary. Hotel chosen, then checkout started. Multi-room completion, meaning two or more rooms selected and then checkout. Time from the first room paint to the first rate selected.
Must not get worse. Support contacts after booking that are about the room, breakfast, or cancellation. Average booking value, and the share that is still refundable. This must not collapse into cheapest non-refundable only. Price mismatch at payment. The total they selected has to be the total they pay.
Signs it is failing. People open the raw supplier name and switch family. The cluster was wrong. Or they tap continue on a rate that is already dead.
I would measure this with a partner-level holdout. Same hotels, same dates, new module against the current list. The read that matters is two-or-more-room completion, and room, breakfast, and cancel contacts, over 28 days. I would not turn it on for every partner at once. Markup rules differ, and a blended number would lie.
28 days is a starting window. If a partner has fewer than a few hundred room-select sessions in that time, I would extend the holdout. A 5% move on 80 bookings is noise.
What ships first, and what waits
First. Room count. Families when there are at least six offers, otherwise a flat list. Chips only when the field is structured. Stay total. Same-room copy. Missing photo. Unknown terms. Sold-out swap. SDK config for tax and for whether non-refundable rates are allowed.
Next. Better clustering from room codes plus name similarity. A cleaner “make this room different.” A price-change banner between select and pay.
Later. Accessibility and connecting-room flags, once suppliers actually send them. A partner-level default-rate policy. Points on the card.
The pricing and tax engine has to return a stay total the screen can trust. Board and refund have to be mapped per supplier. That is operations work. Stale inventory is the operational risk. Partner config has to exist, or every embed will fork.
Clustering is not learned. It is room code, name similarity, occupancy, and beds. A hotel that joined last week gets the same rule as a mature one. If the rule is unsure, the list flattens. “Sure” means the structured fields matched. It does not mean this cluster has booked well before. A new supplier with no history does not get a broken cluster. It gets the fallback.
Where this breaks
Breakfast or cancel missing: no chip. The review says unconfirmed. They can still book. I do not infer it from the room name. Names lie. A title with “BB” in it can still be room only.
No photos: the hotel photo, plus “Room photo unavailable.” The room stays in the list.
Cannot cluster: a flat list, chips, sorted by stay total. Longer, and honest.
Two suppliers, same-looking room, different price: show the cheaper live rate for that chip set. Hide the supplier.
The rate dies while they are still on the screen: kill the button. Offer the next live rate in the same family. Recalculate the total.
The price changes before they pay: a banner with the old total and the new one. They confirm, or they go back. Never a silent change.
Odd occupancy, like one adult and two rooms: allowed. A split was suggested. Unusual baskets are not blocked.
The room sleeps fewer people than the party: warn once. Do not block. A child often shares, and hotels are inconsistent about this.
The partner forbids non-refundable: those rates never render. No greyed-out rows.
Twenty or more rate plans: families collapse them. If they cannot be clustered, show eight, then “more rates,” with the cheapest and the flexible one pinned.
If the product cannot say something, it does not say it. The traveler can still move.
What I would want to learn from real usage
The six-offer cutoff for clustering is the thing I trust least. I would watch how often people open the raw name and switch family, on the first partner, before I treat that number as a rule.
I would also want to know whether “same setup in every room” feels like a help or like the product decided for them. The opt-in to mix rooms is easy to hide. If families keep backing out to change room two, the default is wrong.
And I would sit with support for a month and read the breakfast and cancel tickets before adding any ranking, any “best pick,” or any model. If those tickets do not fall, the chips are still lying, and a smarter sort will not fix that.