Overview

  • Primary JSON endpoints:
    • GET /api/events
    • GET /api/tenders
  • Supporting routes:
    • GET /api/image/:kind/:pageId
    • POST /api/sync-tender-qrs
  • GET /tender/:id is 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

ParamDefaultNotes
window7Number 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 TV is checked
  • Location contains "pub" (case-insensitive)
  • Event Start lands on the worker’s todayISO when interpreted in America/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"
}
FieldTypeNotes
titlestringEvent name from Notion
startstringFormatted start time
endstringFormatted end time; derived from Event End, Duration (hrs), or a 1-hour fallback
statusstringComputed at request time: "Now", "Next", "Later", or "Done"
startIsostringUTC ISO 8601 start timestamp used for client-side re-evaluation
endIsostringUTC ISO 8601 end timestamp
rcTagstring or null"rc-direct", "rc-affiliate", or omitted

Status freshness

status is computed by the worker at request time. The TV display client re-evaluates status locally between API refreshes using startIso, endIso, and the server-aligned clock derived from lastUpdated.

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 >= tomorrowISO and < 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=14 to 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
}
FieldTypeNotes
titlestringEvent name
datestringFormatted date label
startstringFormatted start time
endstringFormatted end time
startIsostringUTC ISO 8601 start timestamp
endIsostringUTC ISO 8601 end timestamp
rcTagstring or nullRC 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
}
FieldTypeNotes
idstringNotion page id
titlestringAnnouncement title from Name
prioritystringLowercased priority label; defaults to "normal"
textstringAnnouncement copy from Content or fallback Name
imageUrlstringSame-origin proxy URL for Picture, Image, or PNG file properties; may be empty
durationnumber or nullOptional 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 announcement slide
  • Every active pub tender with Meet your Tender checked becomes a tender slide

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..."
}
FieldTypeNotes
idstringStable slide identifier
typestringannouncement or tender
titlestringSlide title
bodystringAnnouncement-only body copy
prioritystringAnnouncement-only styling hint
durationMsnumber or nullAnnouncement-only slide duration in milliseconds
namestringTender-only full name
imageUrlstringSame-origin proxy URL for either the announcement image or tender profile photo
qrImageUrlstringTender-only pre-rendered QR image URL from the Notion QR file property
qrUrlstringTender-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 qrUrl metadata, but the TV currently renders only qrImageUrl
  • 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"
}
FieldTypeNotes
datestringISO date (YYYY-MM-DD) in Eastern time
openbooleanfalse if pub is closed that day
hoursstring or nullFormatted 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": []
}
FieldTypeNotes
namestringFirst name(s) of all on-duty tenders joined with " & "
hoursstringOperational timeframe from the Shifts DB
notestringHuman-readable shift state label
noteStatestringMachine-readable state
pageUrlstringURL metadata from the first tender page with a URL property
activeTendersarrayProfile-slide data for tenders with Meet your Tender checked

noteState values

ValueMeaning
upcomingShift starts later today
setupWithin full shift window, before operational hours begin
activeWithin operational hours
last-callWithin lastCallMinutes minutes of operational end
closedAfter operational hours but still within the full shift
inactiveNo 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.

FieldTypeNotes
idstringNormalized tender id
fullNamestringFull name from the tender’s Notion page title
showProfilebooleanWhether the tender should appear in slideshow profile slides
imageUrlstringSame-origin proxy URL for the Picture file
qrImageUrlstringSame-origin proxy URL for the QR file
pageUrlstringURL 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_ID when present
  • Otherwise it walks recent shift assignment tender relations until it finds the tender database parent id
  • Only tender pages whose Active checkbox is true are returned
  • Results are sorted alphabetically by firstName, then fullName

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"
}
FieldTypeNotes
idstringNormalized Notion id used by the frontend and redirect route
notionPageIdstringOriginal Notion page id
firstNamestringDerived from the first token of fullName
fullNamestringDisplay name from the tender page
zellestringRaw Zelle value from Notion; frontend treats email and phone values specially
venmostringRaw Venmo value from Notion; frontend accepts either a full Venmo URL or an @handle-style value
biostringDirectory modal biography text
imageUrlstringTender profile image
pageUrlstringFirst resolved URL property from the tender page
profileUrlstringCanonical 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:

  • announcement
  • tender
  • tender-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=1 returns 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, and Short.io Link ID properties when configured
  • Uses PUBLIC_SITE_URL to 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, or NOTION_PUB_TENDERS_DATABASE_ID are missing, the response keys still exist but may contain empty arrays or fallback values. Clients should handle this gracefully.