Club externo
A booking engine sends the guest to a branded GuestMaker login and gets them back signed in, with no OIDC library and no redirect-URI registration per search page. Use this when your return URL is dynamic (it carries the guest’s dates, hotel and occupancy) and an exact-match redirect_uri cannot express it. If your return URL is a single fixed callback, prefer the OIDC quickstart.
- We issue a
client_idand aclient_secret(shown once), and register an allowlist of return hosts: not exact URLs. Any path and query on an allowlisted host is accepted, so your search context survives. http://localhostis accepted as a return host for development. It is the only non-HTTPS host we allow, and it exists so you can run a real integration locally.- The exchange and profile calls run on your backend only. The
client_secretnever reaches a browser. - We tell you your delivery mode:
code,fragment, orbothwhile you migrate. Handle whichever you are set to: step 2 covers all three.
1. Send the guest to the login
Encode the return URL as a whole. Its own ? and & would otherwise be read as parameters of the login URL, silently truncating where the guest comes back to. redirectUrl and locale are also accepted as domain and lang.
const returnUrl = "https://book.hotel.com/rates?checkin=2026-10-01&adults=2";
const login = new URL("https://www.guestmaker.ai/loyalty/club/login");
login.searchParams.set("client_id", process.env.GM_CLIENT_ID);
login.searchParams.set("redirectUrl", returnUrl); // encoded for you by searchParams
login.searchParams.set("locale", "pt"); // pt | es | en | fr | de | it
redirect(login.toString());2. They return with a code or a token
The guest lands back on your return URL, with the handoff attached according to your delivery mode. A fragment is never sent to a server: if you are on fragment, only the browser can read it. Never copy it into a query string to work around that: query strings are recorded by every proxy and access log in the path, which is the whole reason hlCode exists.
// ?hlCode=<32 chars> → your server reads it (delivery: code)
// #hlToken=<jwt> → only the browser sees it (delivery: fragment)
// both → prefer the CODE and ignore the fragment
const code = new URL(req.url).searchParams.get("hlCode");
if (code) return exchange(code); // step 33. Exchange the code
Single use, five minutes, bound to the client it was issued to. Every rejection answers a flat invalid_grant: unknown, expired, already redeemed and wrong-client are deliberately indistinguishable, so a failure tells you to re-run the login rather than to retry.
const res = await fetch("https://www.guestmaker.ai/api/loyalty/club/exchange", {
method: "POST",
headers: {
"X-Client-Id": process.env.GM_CLIENT_ID,
"X-Client-Secret": process.env.GM_CLIENT_SECRET,
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
});
const { token, expires_in, member } = await res.json();4. Read the member profile
The exchange already returns the member, so this is for later reads within the token’s lifetime. It needs the Bearer and the client credentials: a stolen token alone cannot pull a guest’s data.
const me = await fetch("https://www.guestmaker.ai/api/loyalty/club/me", {
headers: {
"X-Client-Id": process.env.GM_CLIENT_ID,
"X-Client-Secret": process.env.GM_CLIENT_SECRET,
Authorization: `Bearer ${token}`,
},
}).then((r) => r.json());
// { id, email, firstName, lastName, tier, language, marketingConsent, ... }The login POST can succeed, returning a 303 with a valid handoff, and the browser can still refuse the final redirect to you. Nothing errors on either side. Check, in order:
- Your return host is on the allowlist. If it is not, the login entry answers
403before any login happens. That is the loud case, and the easy one. - The browser console at the moment you press sign in. A
form-actionContent-Security-Policy violation names the submit URL, not the redirect that was actually blocked: so it reads as though the POST failed when the POST is exactly what succeeded. Tell us: this is ours to fix, not yours. - The login is 15 minutes long. A guest who leaves the tab and comes back gets a fresh start, not a stale session.
examples/loyalty-club-rp/server.mjs: one file, no dependencies.A fake booking engine that implements everything above: both parameter spellings, all three delivery modes, the fragment handed back from the browser, and the profile read. It runs on localhost, which we allowlist for exactly this.
GM_CLIENT_ID=… GM_CLIENT_SECRET=… node server.mjs # → http://localhost:4321
On startup it performs a real login entry against your configuration and reports whether the handoff can complete at all: before you click anything. Ask us for it with your sandbox credentials.