Skip to content

Club externo

Signed handoff · no OIDC client

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.

Before you start
You integrate as a confidential client.
  • We issue a client_id and a client_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://localhost is 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_secret never reaches a browser.
  • We tell you your delivery mode: code, fragment, or both while you migrate. Handle whichever you are set to: step 2 covers all three.
Step 1
Send the guest to the login
Step 2
They return with a code or a token
Step 3
Exchange the code on your backend
Step 4
Read the member profile

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 3

3. 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, ... }
If the guest authenticates and never arrives
The failure mode of this flow is silence, not an error.

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 403 before any login happens. That is the loud case, and the easy one.
  • The browser console at the moment you press sign in. A form-action Content-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.
A working engine you can run
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.