API reference
GM Feed
The five situations that need a GM call right now, and the acknowledgement that clears a vendor card.
The GM Feed is the pocket view: approvals waiting, requests past their SLA, today's departure pressure, the groups on property, and any vendor that stopped answering. Hotel Operator serves it on its own /gm route and polls it every few seconds.
Every card is computed on read from state the platform already keeps, and every card is self-clearing. Approving the offer removes the approval. Sending the departing-today offer removes the last-day card. The only card with an explicit acknowledgement is the vendor card, which is what POST /v1/gm/acks is for.
GET /v1/gm/feed
The five card lists, computed fresh on every call.
Auth: unauthenticated in this build.
Request
GET /v1/gm/feed
GET /v1/gm/feed
No path params, no query params, no body.
Response
JSON
{
"approvals": [
{
"offer_id": "3c2b1a09-8d7e-4f60-b1c2-5a4e3d2f1b09",
"label": "Same-day spa slot",
"price_usd": 180,
"audience_line": "1 guest, named",
"requested_by_first_name": "Marco",
"requested_at": "2026-08-30T18:00:00Z",
"intent_note": "You asked about the spa."
}
],
"overdue": [
{
"request_id": "0d6b8c21-7f3a-4a6e-9c55-1b2f0a9de334",
"type_label": "dining",
"guest_first_name": "John",
"queue_id": "dining",
"queue_label": "Dining",
"age_minutes": 90,
"sla_minutes": 15,
"assign_to": { "staff_id": "dana", "first_name": "Dana" }
}
],
"last_day": {
"departures_today": 1,
"arrivals_today": 2,
"band": "high",
"recommended_price_usd": 60,
"eligible_departing": 1
},
"groups": [
{ "group_id": "1f0a7c4e-6d0b-4f42-9c31-6a1a4d1b93f2", "label": "Aretha's crew", "member_count": 1, "open_votes": 0 }
],
"vendors": [
{ "source": "stayntouch", "failures": 2, "last_failed_at": "2026-08-30T17:55:00.000Z" }
]
}
Every list is an array and is empty when nothing qualifies. last_day is the one nullable field: it is null whenever there is no card to show.
approvals
Offers sitting in pending_approval, newest first. An offer lands there when its price is strictly above the configured GM threshold, which is 150 USD in the shipped config. audience_line is a rendered phrase, not structured data: "1 guest, named" or "4 guests, named" for a named audience, otherwise the audience filters joined together ("gold and platinum, parties of 4+, departing today"), or "everyone eligible" when there are no filters. requested_by_first_name falls back to the raw created_by staff id when the staff row is missing. intent_note is the guest's own words when the offer carried one, otherwise null.
overdue
Escalated open requests, oldest first, capped at 10. age_minutes is whole minutes since creation, sla_minutes is the queue's configured SLA. assign_to names the first configured staff member on that queue, so the card always has an action attached. A queue with no configured staff is skipped entirely rather than rendered without an assignee.
type_label uses the request-type labels from activity.ts. Type other renders as a request.
last_day
Present only when the Last Day band is not low and no departing-today offer is currently open. Same numbers as GET /v1/last-day/summary, minus pressure.
groups
Every live group with its active member_count and open_votes, the number of going votes on activities still open. Informational, no action attached.
vendors
source_unreachable events inside the configured window (15 minutes in the shipped config), grouped by source. A source disappears once acknowledged. A failure that arrives after the acknowledgement brings the source back, counting only the failures after the ack.
Errors
None. The route always returns 200.
POST /v1/gm/acks
Acknowledge a card so it stops showing. In this build only the vendor cards read acks back.
Auth: unauthenticated in this build.
Request
POST /v1/gm/acks
POST /v1/gm/acks
content-type: application/json
| Field | Type | Required | Meaning |
|---|---|---|---|
kind | string | required | Card family. GET /v1/gm/feed reads only vendor. |
key | string | required | What is being acknowledged. For a vendor card, the source string from the card. |
staff_id | string | required | Who acknowledged it. Stored, not validated against the staff table. |
JSON
{ "kind": "vendor", "key": "stayntouch", "staff_id": "jake" }
Response
JSON
{ "ok": true }
Errors
| Status | Error | When |
|---|---|---|
| 400 | kind, key, and staff_id required | Any of the three fields is missing or empty. |
Notes
- Acks are append-only. Each call inserts a row, and the feed reads the latest
acked_atper key. There is no un-ack. - An ack covers failures up to its own timestamp. Anything newer resurfaces the source with a fresh count, which is why a flapping vendor keeps coming back instead of staying silent.
kindaccepts any string. A value other thanvendoris stored and then ignored by the feed, so passing one clears nothing.- The approvals list and the offer approve and reject routes are two halves of one flow. The feed shows what is pending,
POST /v1/offers/:id/approveandPOST /v1/offers/:id/rejectact on it.