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:

FamilyWhat it covers
Identity and staysRecognition, the confirmation email front door, consent, the guest home, the operator context view
ConciergeThe single conversational endpoint on the guest web app
Requests and queuesPlacing a request, the staff roster, queues, claim, complete, decline
HousekeepingQuiet Hours: the guest's room preferences, the floor board, presence-aware attendant routes, serviced and do-not-disturb
Openings and offersThe Usher's openings read, offer plays, audience preview, create, approve, close, accept
GroupsShared plans, join codes, proposed activities, attendance, booking
Last DayThe departure-pressure summary
Property twinThe Property tab summary
GM feedThe five situations needing a GM call, and acknowledgements
ReportThe funnel report and its CSV export
Activity and eventsThe rendered activity stream and the event sink
TourThe 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:

MessageStatus
unauthorized401
organizer_only403
anything starting unknown_404
invalid_state, offer_expired, sold_out, empty_audience, already_in_group, consent_required, group_dissolved, nobody_going409
everything else400

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.