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
FieldTypeRequiredMeaning
kindstringrequiredCard family. GET /v1/gm/feed reads only vendor.
keystringrequiredWhat is being acknowledged. For a vendor card, the source string from the card.
staff_idstringrequiredWho acknowledged it. Stored, not validated against the staff table.

JSON

{ "kind": "vendor", "key": "stayntouch", "staff_id": "jake" }

Response

JSON

{ "ok": true }

Errors

StatusErrorWhen
400kind, key, and staff_id requiredAny 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_at per 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.
  • kind accepts any string. A value other than vendor is 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/approve and POST /v1/offers/:id/reject act on it.