Guest verification flow
This guide explains how to use the in-house guest list in a captive portal: matching what the guest types against the list, when the Wi-Fi session should end, how often to refresh the list, and which fields to keep for Law 5651 records.
What to ask on the portal#
Because the list provides room and identity details together, the most common form is room number + surname. The room number alone is not enough; anyone who knows a neighbouring room’s number could log in. Alternatives are room number + ID/passport number or surname + date of birth; keep the latter as a fallback for guests whose ID number is empty.
Matching rules#
Normalise both sides before comparing what the guest typed with the values in the list:
- Room number: trim whitespace and convert to upper case.
roomNameis a string (it may be"104","A-12"or"Villa 3"); do not convert it to a number or drop leading zeros. - Surname: convert to upper case (mind the Turkish
i → İmapping) and collapse repeated spaces. List values are usually read from the ID document in upper case. - ID number: remove spaces and convert to upper case. A Turkish ID number has 11 digits; a passport number mixes letters and digits.
- Date of birth: the list gives
DD.MM.YYYY; convert if your form collects another format.
const norm = (s) => String(s ?? "")
.replace(/\s+/g, " ")
.trim()
.toLocaleUpperCase("tr-TR");
function findGuest(list, room, lastName) {
return list.find((g) =>
norm(g.roomName) === norm(room) && norm(g.lastName) === norm(lastName)
) || null;
}
const guest = findGuest(data.otelde, form.room, form.lastName);
if (!guest) {
// "Room number and surname did not match" — count failed attempts, throttle after 5
}The unique value of the matching record is the hotspot user’s identity. When the same guest logs in from a second device, reuse the account tied to this value instead of creating a new one, and enforce the device limit on that account.
Session lifetime#
checkout is the planned check-out date and points to 00:00 (UTC) of that day; it has no time of day. If you open the session until that moment, the guest loses access on the morning of departure. Compute the session end by adding the hotel’s check-out time to the date:
const CHECKOUT_TIME = "12:00"; // the hotel’s check-out time
const day = new Date(guest.checkout * 1000)
.toLocaleDateString("en-CA", { timeZone: "UTC" }); // "2026-09-10"
const sessionEnd = new Date(`${day}T${CHECKOUT_TIME}:00+03:00`);If the guest leaves before check-out time they drop off the list; close the account on the next sync. If the stay is extended, checkout changes; compare this field during sync and update the session end. For late check-outs it is safer to keep access open as long as the guest remains in the list.
Periodic sync#
The list is a snapshot; there is no change notification (webhook). Fetch the list at a fixed interval and diff it against the previous one by unique:
- Interval: 5 minutes is enough for most hotels. If guests are expected to get online right at check-in you can go down to 1–2 minutes; do not go below 1 minute.
- New
unique: prepare the account; the guest is matched at the first portal login. - Missing
unique: the guest checked out or the reservation was cancelled. End the session and close the account. - Same
unique, differentroomNameorcheckout: a room move or an extension. Update the account. - Failed request: on a timeout,
401or5xx, keep the last successful list; never close accounts based on an error response.
async function sync(prev) {
const res = await fetch(BASE + "/public/json/customer/inhotel", {
method: "POST",
headers: { "ApiKey": process.env.HMS_API_KEY, "HotelCode": HOTEL_CODE }
});
if (!res.ok) return prev; // 401 / 5xx: keep the old list
const { success, otelde } = await res.json();
if (success !== 1) return prev;
const next = new Map(otelde.map((g) => [g.unique, g]));
for (const [id] of prev) {
if (!next.has(id)) await closeAccount(id); // checked out
}
for (const [id, g] of next) {
const old = prev.get(id);
if (!old) await openAccount(g); // new guest
else if (old.checkout !== g.checkout || old.roomName !== g.roomName) await updateAccount(g);
}
return next;
}Several guests in one room#
Every guest in a room is a separate record: rezervasyon_id is shared, unique differs. Open accounts per guest rather than per room, so each guest has their own device, session and Law 5651 record. A room number + surname query can return more than one record for guests with the same surname (a family); instead of taking the first match, disambiguate by first name or date of birth, or open a shared account for them.
Law 5651 records#
The obligation to keep access logs under Law 5651 lies with the hotspot system; HMS only supplies the identity data. When a session opens, store the following fields together with the MAC/IP address and timestamp: unique, rezervasyon_id, firstName, lastName, identityNumber, birthDate, roomName, checkin, checkout. Once a guest drops off the list HMS no longer provides this information; keep the record on your side.