Guide
Getting started
The shape of the Guest Intelli core, its three auth modes, its error envelope, and a first walkthrough from booking code to a card sitting in a staff queue.
What the core is
The Guest Intelli core is one Fastify 5 service on Node 22, backed by a single SQLite file. It has no queue, no worker, no cache tier, and no second process. Everything the platform knows lives in one database, and every read is computed at request time from that database.
The unit of everything is the stay. One guest stay is one row in stays,
identified by a stay_id, reached by a booking_code. Requests, offers,
groups, bookings, actions, and events all hang off a stay_id. There is no
separate guest record and no account. A stay ends and its row stays put.
The service registers 52 routes, grouped into 12 families. Each family has its own reference page:
| Family | What it covers |
|---|---|
| Identity and stays | Recognition, the confirmation email front door, consent, the guest home, the operator context view |
| Concierge | The single conversational endpoint on the guest web app |
| Requests and queues | Placing a request, the staff roster, queues, claim, complete, decline |
| Housekeeping | Quiet Hours: the guest's room preferences, the floor board, presence-aware attendant routes, serviced and do-not-disturb |
| Openings and offers | The Usher's openings read, offer plays, audience preview, create, approve, close, accept |
| Groups | Shared plans, join codes, proposed activities, attendance, booking |
| Last Day | The departure-pressure summary |
| Property twin | The Property tab summary |
| GM feed | The five situations needing a GM call, and acknowledgements |
| Report | The funnel report and its CSV export |
| Activity and events | The rendered activity stream and the event sink |
| Tour | The scripted walkthrough state machine |
Two surfaces sit on top of these routes: the Concierge (the guest web app) and Hotel Operator (the staff web app).
Base URL
Every path below is relative to your Guest Intelli host. The service binds
0.0.0.0 and reads PORT, defaulting to 4700, so a local run answers on
http://localhost:4700. Deployments set their own host. All routes live under
/v1.
Three auth modes
Guest session token. POST /v1/recognize takes a booking code and returns a
session_token, a fresh UUID stamped on the stay row. Send it back as
x-gi-session on any route that acts as that guest. The check is exact: the
token must match the session_token column for that stay_id, or the route
returns 401 unauthorized. Recognizing the same booking code again issues a new
token and invalidates the old one. Session-guarded routes include the guest
home, consent, the Concierge, placing a request, every group write, accepting an
offer instance, and opening a concierge suggestion.
Service token. The two routes QR Find calls inbound (POST /v1/federation/porter/escalations, POST /v1/federation/porter/upsells) require
x-gi-service-token to equal the deployment's configured federation token. If no
token is configured, these routes return 401 for every caller, including one
that guesses right. There is no partial credit and no other route accepts this
header.
Unauthenticated in this build. Every operator and read-only route is open:
the queues, the staff roster, claim, complete, decline, the property summary,
the activity stream, openings, the GM feed, the report and its CSV, the offer
create and approve routes, the tour, the confirmation email front door, the
operator context view, and the event sink. That is a demo-build posture, stated
plainly so nobody deploys it to a public host and assumes otherwise. The context
view does strip the guest's session_token, booking_code, and porter_ref
from its response, so reaching it does not hand you the guest's session.
CORS
server.ts sets three headers on every response through an onSend hook:
HTTP
access-control-allow-origin: *
access-control-allow-headers: content-type, x-gi-now, x-gi-session
access-control-allow-methods: GET, POST, OPTIONS
A wildcard OPTIONS /* handler answers preflight with 204 and those same
headers. Note what is missing: x-gi-service-token is not in the allow-headers
list. Federation is a server-to-server call by design, and a browser
cannot make it.
Errors
Every failure returns the same envelope, a single key:
JSON
{ "error": "unknown_booking_code" }
The status comes from the message string, mapped in errorStatus:
| Message | Status |
|---|---|
unauthorized | 401 |
organizer_only | 403 |
anything starting unknown_ | 404 |
invalid_state, offer_expired, sold_out, empty_audience, already_in_group, consent_required, group_dissolved, nobody_going | 409 |
| everything else | 400 |
Route-level validation short-circuits before that mapping and returns 400 with
its own message, for example { "error": "booking_code required" } or
{ "error": "invalid_message" }. One route maps its own statuses:
POST /v1/tour/advance returns 409 for not_satisfied and stale_step, and
400 otherwise.
The test clock header
x-gi-now pins the service's idea of "now" to an ISO timestamp for that one
request. It exists for tests and the demo simulator. Real callers omit it, and
an unparseable value is ignored.
First ten minutes
Recognize a stay. SOL-655 is Aretha, a gold-tier party of nine in the seeded
fixtures.
BASH
curl -X POST https://<your-guest-intelli-host>/v1/recognize \
-H 'content-type: application/json' \
-d '{"booking_code":"SOL-655"}'
JSON
{
"stay": {
"stay_id": "stay-655",
"guest_first_name": "Aretha",
"room": "655",
"tier": "gold",
"party_size": 9,
"analytics_consent": null
},
"session_token": "..."
}
analytics_consent of null means nobody has asked yet. Record an answer, which
also opens the consent-gated parts of the home:
BASH
curl -X POST https://<your-guest-intelli-host>/v1/stays/stay-655/consent \
-H 'content-type: application/json' -H 'x-gi-session: <token>' \
-d '{"analytics_consent":1}'
Read the home. It returns the public stay, recommendations, this stay's requests
newest-first, its offers, its group, a last_day boolean, federated service deep links
(concierge_url and timelens_url, both null unless the guest consented and
the deployment configured them), concierge suggestions, and the Concierge's chips
and usher line.
BASH
curl https://<your-guest-intelli-host>/v1/stays/stay-655/home \
-H 'x-gi-session: <token>'
Place a request. Nine people qualifies for the group_dining recommendation, so
pass its id along to link the request back to what prompted it.
BASH
curl -X POST https://<your-guest-intelli-host>/v1/requests \
-H 'content-type: application/json' -H 'x-gi-session: <token>' \
-d '{"stay_id":"stay-655","type":"dining",
"details":{"party_size":9,"preferred_time":"19:30"},
"recommendation_id":"<id from home>"}'
The response is { "request": { ... } } with status: "open" and
queue_id: "dining". Routing from request type to queue comes from the property
config, not the caller.
Watch it land. This read needs no session:
BASH
curl https://<your-guest-intelli-host>/v1/queues/dining/requests
The top card carries guest_first_name: "Aretha", the room, the tier, the party
size, an escalated flag computed against the queue's SLA, and the group label
when the stay belongs to one. Claim it, then complete it:
BASH
curl -X POST https://<your-guest-intelli-host>/v1/requests/<id>/claim \
-H 'content-type: application/json' -d '{"staff_id":"dana"}'
curl -X POST https://<your-guest-intelli-host>/v1/requests/<id>/complete \
-H 'content-type: application/json' \
-d '{"staff_id":"dana","venue":"chefs_table","slot":"19:30"}'
Completion writes a booking with a confirmation code, stamps result_value_usd
on the request, records an action, marks the linked recommendation acted, and
emits events at each step. If a folio sink is configured, the charge posts to the
property system from here. That path is covered in
Property systems, and what the events do next is
covered in Events and the ledger.