Overview
- Primary JSON endpoints:
GET /api/eventsGET /api/tenders
- Supporting routes:
GET /api/image/:kind/:pageIdPOST /api/sync-tender-qrs
GET /tender/:idis a compatibility redirect route used for deep linking into the tender directory UI- Both API responses are cached at the Cloudflare edge for 2 minutes
- Both endpoints fall back to placeholder or empty data when Notion credentials are not configured
Sources: worker.js (handleEventsApi, handleTendersApi, PLACEHOLDER block)
GET /api/events
Query params
| Param | Default | Notes |
|---|---|---|
window | 7 | Number of calendar days ahead to include in weekEvents. Capped at 30. The TV uses the default; /events/ requests ?window=14. Each distinct value is cached independently at the edge. |
Response shape
{
"todayEvents": [],
"weekEvents": [],
"announcements": [],
"pubHoursWeek": [],
"pubTender": {},
"slides": [],
"lastUpdated": "2026-03-26T02:20:00.000Z",
"source": "notion"
}source is "notion" when live data is returned and "placeholder" when the worker is serving fallback data.
lastUpdated is also the server timestamp the TV client uses to keep slideshow and featured-event state aligned across multiple screens.
Event object (todayEvents)
Today’s pub events, sorted by start time. This array powers the TV display’s Happening at the Pub panel.
Eligibility rules
An event is included in todayEvents only if all of the following are true:
Include on Pub TVis checkedLocationcontains"pub"(case-insensitive)Event Startlands on the worker’stodayISOwhen interpreted inAmerica/New_York
To make that local-date rule reliable, the worker intentionally queries a wider Notion window first and then narrows the result set in Eastern time.
Sources: worker.js (fetchFromNotion, isAtPub, isOnLocalDate)
{
"title": "Trivia Night",
"start": "8:30 PM",
"end": "10:00 PM",
"status": "Later",
"startIso": "2026-01-18T01:30:00.000Z",
"endIso": "2026-01-18T03:00:00.000Z",
"rcTag": "rc-direct"
}| Field | Type | Notes |
|---|---|---|
title | string | Event name from Notion |
start | string | Formatted start time |
end | string | Formatted end time; derived from Event End, Duration (hrs), or a 1-hour fallback |
status | string | Computed at request time: "Now", "Next", "Later", or "Done" |
startIso | string | UTC ISO 8601 start timestamp used for client-side re-evaluation |
endIso | string | UTC ISO 8601 end timestamp |
rcTag | string or null | "rc-direct", "rc-affiliate", or omitted |
Status freshness
statusis computed by the worker at request time. The TV display client re-evaluates status locally between API refreshes usingstartIso,endIso, and the server-aligned clock derived fromlastUpdated.
Sources: worker.js (transformPage, deriveStatus)
WeekEvent object (weekEvents)
Upcoming events for the next N calendar days (default 7, controlled by the ?window= param), sorted by start time. This array powers the TV display’s Coming Up at the Pub panel and the /events/ page.
- Uses the same pub-location and TV-include gates as
todayEvents - Includes rows with
Event Start >= tomorrowISOand< todayISO + (window + 1) days - Starts at calendar tomorrow, so same-day events are not duplicated there
- The TV client removes any row whose live status has already become
Done - The
/events/page requests?window=14to show a 14-day horizon; the TV uses the default 7-day window
Sources: worker.js (fetchFromNotion, transformWeekPage), public/tv/script.js (renderWeekEvents), public/events/index.html
{
"title": "Open Mic Night",
"date": "Wed, Jan 21",
"start": "9:00 PM",
"end": "11:00 PM",
"startIso": "2026-01-22T02:00:00.000Z",
"endIso": "2026-01-22T04:00:00.000Z",
"rcTag": null
}| Field | Type | Notes |
|---|---|---|
title | string | Event name |
date | string | Formatted date label |
start | string | Formatted start time |
end | string | Formatted end time |
startIso | string | UTC ISO 8601 start timestamp |
endIso | string | UTC ISO 8601 end timestamp |
rcTag | string or null | RC classification tag |
Announcement object (announcements)
Rows are included in announcements only when Include on Pub TV is checked in the Announcements database.
{
"id": "9cb8...",
"title": "Reminder",
"priority": "high",
"text": "The pub will close early this Friday at 10 PM.",
"imageUrl": "https://pub.ihnyc-rc.org/api/image/announcement/9cb8...",
"duration": 8
}| Field | Type | Notes |
|---|---|---|
id | string | Notion page id |
title | string | Announcement title from Name |
priority | string | Lowercased priority label; defaults to "normal" |
text | string | Announcement copy from Content or fallback Name |
imageUrl | string | Same-origin proxy URL for Picture, Image, or PNG file properties; may be empty |
duration | number or null | Optional seconds value used by the TV slideshow |
Sources: worker.js (fetchAnnouncementsFromNotion)
Slide object (slides)
The worker synthesizes slides from two existing sources:
- Every qualifying announcement becomes an
announcementslide - Every active pub tender with
Meet your Tenderchecked becomes atenderslide
The client uses this array to drive the left-panel slideshow.
{
"id": "announcement-1",
"type": "announcement",
"title": "Reminder",
"body": "The pub will close early this Friday at 10 PM.",
"imageUrl": "https://pub.ihnyc-rc.org/api/image/announcement/9cb8...",
"priority": "high",
"durationMs": 8000
}{
"id": "tender-1",
"type": "tender",
"title": "Meet your Tender",
"name": "Alex Smith",
"imageUrl": "https://pub.ihnyc-rc.org/api/image/tender/2ac2...",
"qrImageUrl": "https://pub.ihnyc-rc.org/api/image/tender-qr/2ac2...",
"qrUrl": "https://pub.ihnyc-rc.org/tenders/?tender=2ac2..."
}| Field | Type | Notes |
|---|---|---|
id | string | Stable slide identifier |
type | string | announcement or tender |
title | string | Slide title |
body | string | Announcement-only body copy |
priority | string | Announcement-only styling hint |
durationMs | number or null | Announcement-only slide duration in milliseconds |
name | string | Tender-only full name |
imageUrl | string | Same-origin proxy URL for either the announcement image or tender profile photo |
qrImageUrl | string | Tender-only pre-rendered QR image URL from the Notion QR file property |
qrUrl | string | Tender-only canonical in-site profile URL metadata; current TV code does not render this directly |
Important current behavior:
- Announcement slides may be text-only, image-only, or mixed image+text cards
- Tender slides do not include bio text in the worker payload
- Tender slides still include
qrUrlmetadata, but the TV currently renders onlyqrImageUrl - The TV and
/announcements/surfaces intentionally consume same-origin proxy image URLs rather than very long signed Notion file URLs
Sources: worker.js (buildSlides, fetchPubTenderFromNotion)
PubHoursDay object (pubHoursWeek)
Array of 7 items, one per day of the current week, indexed 0-6 from Sunday through Saturday.
{
"date": "2026-03-17",
"open": true,
"hours": "9:00 PM - 11:40 PM"
}| Field | Type | Notes |
|---|---|---|
date | string | ISO date (YYYY-MM-DD) in Eastern time |
open | boolean | false if pub is closed that day |
hours | string or null | Formatted operational timeframe; null if closed |
Sources: worker.js (fetchPubHoursWeek)
PubTender object (pubTender)
{
"name": "Alex & Jordan",
"hours": "9:00 PM - 11:40 PM",
"note": "On duty now",
"noteState": "active",
"pageUrl": "https://notion.so/...",
"activeTenders": []
}| Field | Type | Notes |
|---|---|---|
name | string | First name(s) of all on-duty tenders joined with " & " |
hours | string | Operational timeframe from the Shifts DB |
note | string | Human-readable shift state label |
noteState | string | Machine-readable state |
pageUrl | string | URL metadata from the first tender page with a URL property |
activeTenders | array | Profile-slide data for tenders with Meet your Tender checked |
noteState values
| Value | Meaning |
|---|---|
upcoming | Shift starts later today |
setup | Within full shift window, before operational hours begin |
active | Within operational hours |
last-call | Within lastCallMinutes minutes of operational end |
closed | After operational hours but still within the full shift |
inactive | No active shift found for today |
Sources: worker.js (fetchPubTenderFromNotion, deriveShiftNote)
ActiveTender object (pubTender.activeTenders)
Used internally by the worker and client to build tender-profile slides.
| Field | Type | Notes |
|---|---|---|
id | string | Normalized tender id |
fullName | string | Full name from the tender’s Notion page title |
showProfile | boolean | Whether the tender should appear in slideshow profile slides |
imageUrl | string | Same-origin proxy URL for the Picture file |
qrImageUrl | string | Same-origin proxy URL for the QR file |
pageUrl | string | URL metadata from the tender page; not surfaced on TV slides today |
GET /api/tenders
Response shape
{
"tenders": [],
"lastUpdated": "2026-03-26T02:20:00.000Z",
"source": "notion"
}source is "notion" when live data is returned and "placeholder" when the worker is serving fallback data.
Tender directory selection rules
- The worker resolves the Pub Tenders database id directly from
NOTION_PUB_TENDERS_DATABASE_IDwhen present - Otherwise it walks recent shift → assignment → tender relations until it finds the tender database parent id
- Only tender pages whose
Activecheckbox is true are returned - Results are sorted alphabetically by
firstName, thenfullName
Sources: worker.js (fetchTenderDirectoryFromNotion, resolveTenderDirectoryDatabaseId)
TenderDirectoryEntry object
{
"id": "2ac2fadc633680eca9ead2958f9bc9aa",
"notionPageId": "2ac2fadc-6336-80ec-a9ea-d2958f9bc9aa",
"firstName": "Alex",
"fullName": "Alex Smith",
"zelle": "alex@example.com",
"venmo": "https://venmo.com/code?...",
"bio": "Resident bartender and trivia host.",
"imageUrl": "https://...",
"pageUrl": "https://...",
"profileUrl": "/tenders/?tender=2ac2fadc633680eca9ead2958f9bc9aa"
}| Field | Type | Notes |
|---|---|---|
id | string | Normalized Notion id used by the frontend and redirect route |
notionPageId | string | Original Notion page id |
firstName | string | Derived from the first token of fullName |
fullName | string | Display name from the tender page |
zelle | string | Raw Zelle value from Notion; frontend treats email and phone values specially |
venmo | string | Raw Venmo value from Notion; frontend accepts either a full Venmo URL or an @handle-style value |
bio | string | Directory modal biography text |
imageUrl | string | Tender profile image |
pageUrl | string | First resolved URL property from the tender page |
profileUrl | string | Canonical in-site deep link for opening this tender profile |
The /tenders/ page uses this endpoint once on load, renders one card per active tender, and opens the modal indicated by the query string or deep-link route.
Sources: worker.js (transformTenderDirectoryPage), public/tenders/app.js
Redirect Route: /tender/:id
This route is not a JSON endpoint. The Worker normalizes the supplied Notion id and returns a 302 redirect to:
/tenders/?tender=<normalized-id>That lets QR codes, copied links, and internal links point at the stable query-based route while the actual UI continues to live inside the modal-backed /tenders/ page. /tender/:id remains as a compatibility redirect for older links.
Sources: worker.js (fetch handler), public/tenders/app.js
GET /api/image/:kind/:pageId
Short same-origin image route used by the TV display and announcements page.
Supported kind values:
announcementtendertender-qr
The worker resolves the Notion page id, looks up the corresponding file property, fetches the upstream image, and returns it with a 2-minute cache header. This exists mainly to keep browser-facing image URLs short and same-origin.
Sources: worker.js (handleImageProxyApi, resolveNotionImageUrl)
POST /api/sync-tender-qrs
Admin-only endpoint that creates or updates Short.io links for active tenders and writes the generated QR SVG back into the Notion QR files property.
Auth
Either of these request forms is accepted:
Authorization: Bearer <TENDER_QR_SYNC_TOKEN>X-Admin-Token: <TENDER_QR_SYNC_TOKEN>
Query params
dryRun=1returns the sync plan without writing to Notion
Behavior
- Skips inactive tenders
- Skips tenders that do not have a valid
Tender ID - Reuses the existing Short.io link id when possible
- Updates the Notion
QR,QR Link, andShort.io Link IDproperties when configured - Uses
PUBLIC_SITE_URLto build the canonical/tenders/?tender=<id>destination
Sources: worker.js (handleTenderQrSyncApi, runTenderQrSync, syncTenderQrForPage)
Integration Notes
Partial configuration
When
NOTION_SHIFTS_DATABASE_ID,NOTION_ANNOUNCEMENTS_DATABASE_ID, orNOTION_PUB_TENDERS_DATABASE_IDare missing, the response keys still exist but may contain empty arrays or fallback values. Clients should handle this gracefully.