API Routes

JSON API endpoints for frontend/backend communication.

Separation of Concerns

API routes handle:

  • JSON request/response formatting
  • HTTP status code management
  • Parameter extraction from URLs
  • Error response standardization

API routes delegate business logic to controllers and return clean JSON responses.

Endpoints

GET /api/health

Purpose: System health check endpoint

Input Data: None

Output Data:

{ status: "healthy" | "unhealthy", timestamp: string, // Additional health metrics from controller }

HTTP Status: Always 200

POST /api/viewing

Purpose: Update page viewing status in blob storage for real-time tracking with immediate Notion sync

Input Data:

{ pageId: string, // Notion page UUID viewing: boolean, // true when actively viewing, false when leaving tabVisible: boolean // Page Visibility API state }

Content-Type Support:

  • application/json - Standard fetch requests
  • text/plain - sendBeacon requests (automatically parsed as JSON)

Processing Flow:

  1. Extract viewing data from request body (handles both JSON and text formats)
  2. Get user email from authentication context
  3. Update blob storage with key glimpse--viewing--{pageId}
  4. If viewing status changed: Immediately sync to Notion page properties
  5. Return success confirmation

Notion Integration:

  • Real-time sync: When viewing status changes, immediately updates Notion page
  • Properties updated: "Session start" (date) - null when not viewing, timestamp when viewing
  • Additional properties: "Viewing start" (date) - set to first-ever view timestamp
  • Cleanup: Cron job runs every minute to mark stale sessions as viewing: false and sync to Notion

Success Response (200):

{ success: true, data: { pageId: string, // Notion page UUID viewing: boolean, tabVisible: boolean, lastUpdate: string, // ISO timestamp userEmail?: string, sessionId: string // Generated session identifier } }

Error Responses:

// 400 - Invalid Request { error: "Page ID is required" | "Invalid request body", details?: string } // 500 - Server Error { error: "Failed to update viewing status", details: string }

GET /api/viewing/:id

Purpose: Get current viewing status for a page

Input Data:

// URL Parameters { id: string // Notion page UUID }

Processing Flow:

  1. Extract page ID from URL parameter
  2. Retrieve viewing status from blob storage
  3. Check for stale data (>30 minutes old)
  4. Return current status or default values

Success Response (200):

{ pageId: string, viewing: boolean, tabVisible: boolean, lastUpdate: string | null, userEmail?: string, sessionId?: string, stale?: boolean // Present if data is >30 minutes old }

Error Responses: Same pattern as other endpoints

GET /api/viewing/:id/stats

Purpose: Get viewing analytics/statistics for a page

Input Data:

// URL Parameters { id: string // Notion page UUID }

Success Response (200):

{ currentlyViewing: boolean, // Active viewing status (excludes stale) lastViewed: string | null, // ISO timestamp of last update currentUser: string | null, // Email of current/last viewer tabVisible: boolean, // Current tab visibility sessionId: string | null // Current/last session ID }

GET /api/demo/:id/properties

Purpose: Get Notion page properties (filtered for UI consumption)

Input Data:

// URL Parameters { id: string // Notion page UUID }

Processing Flow:

  1. Extract page ID from URL parameter
  2. Call getDemoProperties(id) controller
  3. Return filtered page properties or error

Success Response (200):

{ id: string, properties: { // All properties except type: "button" [propertyName: string]: { type: "title" | "rich_text" | "number" | "select" | "multi_select" | "date" | "checkbox" | "url" | "email" | "phone_number" | "formula" | "relation" | "rollup" | "created_time" | "created_by" | "last_edited_time" | "last_edited_by", // Property-specific value structure } }, parent: { type: "database_id" | "page_id", database_id?: string, page_id?: string }, created_time: string, last_edited_time: string, // ... other Notion page fields }

Error Responses:

// 400 - Invalid Request { error: "Page ID is required", details?: string } // 500 - Server Error { error: "Failed to fetch page data", details: "Notion API error message" }

GET /api/demo/:id

Purpose: Get complete Notion page with content blocks

Input Data:

// URL Parameters { id: string // Notion page UUID }

Processing Flow:

  1. Extract page ID from URL parameter
  2. Call getDemoFull(id) controller
  3. Return complete page with blocks or error

Success Response (200):

{ // Page properties (same as /properties endpoint) id: string, properties: { [key: string]: NotionPropertyValue }, parent: NotionParent, created_time: string, last_edited_time: string, // Block content (hierarchical structure) blocks: [ { id: string, type: "paragraph" | "heading_1" | "heading_2" | "heading_3" | "bulleted_list_item" | "numbered_list_item" | "to_do" | "toggle" | "child_page" | "child_database" | "embed" | "image" | "video" | "file" | "pdf" | "bookmark" | "callout" | "quote" | "equation" | "divider" | "table_of_contents" | "column" | "column_list" | "link_preview" | "synced_block" | "template" | "link_to_page" | "table" | "table_row" | "unsupported", created_time: string, last_edited_time: string, has_children: boolean, children?: NotionBlock[], // Recursive structure for nested blocks // Type-specific content [blockType]: { // Block-specific properties } } ] }

Error Responses: Same as /properties endpoint

Error Handling Pattern

All API routes follow consistent error handling:

// In route handler const result = await controllerFunction(params); if (!result.success) { return c.json({ error: result.error, details: result.details }, result.error === "Page ID is required" ? 400 : 500); } return c.json(result.data);

Data Filtering

API routes return filtered data for UI consumption:

  • Button properties removed: Notion button properties are filtered out to prevent UI confusion
  • Raw Notion format: Other properties maintain their original Notion API structure
  • Complete hierarchy: Block content includes full nested structure with children

Usage Examples

# Get page properties only curl https://your-val.web.val.run/api/demo/abc123/properties # Get complete page with content curl https://your-val.web.val.run/api/demo/abc123 # Health check curl https://your-val.web.val.run/api/health