Overview

Dashboard Plugin: v2.5.0+  |  API Namespace: /wp-json/irf/v1/  |  Last updated: February 2026

The IRF (In Room Fitness) platform provides hotel guests with on-demand workout videos via in-room equipment trays. The system consists of:

The Dashboard exposes a REST API at /wp-json/irf/v1/ that microsites (and the tvOS app) use to fetch video content, submit analytics, and handle guest interactions. All communication uses HTTPS with API key authentication.

Architecture

CENTRAL DASHBOARD inroomfitness.com/wp-json/irf/v1/ REST API Vimeo Sync API Key Authentication HOTEL MICROSITE WordPress + Vue.js hotel.inroomfitness.com Syncs content, submits analytics tvOS APP Native Swift / SwiftUI Apple TV per hotel room Same API, no QR scanning 📱 QR 📺 TV

Data Flow Summary

DirectionDataUsed By
Dashboard → Client Videos, box types, equipment, branding, FAQs, settings, taxonomies Microsite + tvOS
Client → Dashboard Analytics events, ratings, profiles, issue reports, equipment requests, heartbeats Microsite + tvOS

Authentication

Each hotel has a unique API key generated in the Dashboard. All authenticated endpoints require the API key via one of these methods:

Option 1: X-API-Key Header (Recommended)

GET /wp-json/irf/v1/sync HTTP/1.1
Host: inroomfitness.com
X-API-Key: your_api_key_here

Option 2: Bearer Token

GET /wp-json/irf/v1/sync HTTP/1.1
Host: inroomfitness.com
Authorization: Bearer your_api_key_here

Option 3: Query Parameter

GET /wp-json/irf/v1/sync?api_key=your_api_key_here
API Key Scope The API key is tied to a specific hotel. All data returned is scoped to that hotel — you will only receive videos, box types, and settings assigned to the authenticated hotel.
tvOS Implementation For the tvOS app, we recommend using the X-API-Key header method. The API key should be securely stored on the device (e.g., Keychain). Each hotel's Apple TV devices will share the same API key — the key identifies the hotel, not the individual device or room.

Authentication Failure Response

{
  "success": false,
  "code": "unauthorized",
  "message": "Invalid or missing API key"
}

Rate Limiting

ScopeLimit
Per API Key100 requests / minute
Per IP Address1,000 requests / hour
Equipment Requests5 requests / IP / 5 minutes
Issue Reports5 reports / IP / 5 minutes

When rate limited, the API returns:

HTTP 429 Too Many Requests

{
  "success": false,
  "code": "rate_limited",
  "message": "Rate limit exceeded. Try again later.",
  "retry_after": 60
}

Error Handling

All error responses follow a consistent structure:

{
  "success": false,
  "code": "error_code",
  "message": "Human-readable error description"
}
HTTP StatusCodeDescription
400bad_requestMissing or invalid parameters
401unauthorizedMissing or invalid API key
403forbiddenIP not in whitelist / insufficient permissions
404not_foundResource not found
429rate_limitedToo many requests
500server_errorInternal server error

tvOS Considerations

How the tvOS App Differs from the Web Microsite The web microsite relies on QR code scanning to unlock video trays. A guest scans a QR code on their equipment tray, which adds a box_type_id to their browser session. The tvOS app will need a different approach since Apple TV doesn't scan QR codes.

Current Web Flow (Microsite)

  1. Guest scans QR code on equipment tray → opens hotel.site/?tray={box_type_id}
  2. Box type ID stored in browser localStorage
  3. Videos filtered: only videos matching unlocked box type IDs are playable
  4. Hotel has "default" box type IDs (pre-unlocked without QR scan)

Proposed tvOS Flow (To Be Confirmed)

  1. Apple TV device is configured per hotel with the hotel's API key
  2. Device may pass a room number or room type to identify which trays are in the room
  3. The API returns all videos for the hotel — the app determines access based on box types assigned
  4. Alternatively, the hotel's "default" box type IDs (configured in the Dashboard) pre-unlock all standard content
Open Questions (Awaiting Hotel Feedback)

What the tvOS App Needs at Minimum

EndpointPurposePriority
GET /syncFetch all videos, box types, branding, settingsRequired
GET /videosFetch videos (lighter than full sync)Required
GET /box-typesUnderstand which trays unlock which videosRequired
GET /taxonomiesCategory/duration filters with iconsRequired
GET /settingsGet default box types, session configRequired
POST /analyticsReport video play/completion eventsRequired
POST /trackReal-time event trackingRecommended
POST /heartbeatReport device online statusRecommended
POST /ratingVideo ratings (1-5 stars)Optional
GET /equipmentEquipment list (if showing equipment info)Optional
POST /issueReport problemsOptional

Content Endpoints

GET /irf/v1/sync Auth Required

Returns the complete hotel dataset in a single request — videos, box types, equipment, branding, FAQs, settings, taxonomies, questions, and content blocks. This is the primary endpoint for initial data load and periodic sync.

tvOS Usage Call this on app launch and periodically (e.g., every 12 hours) to keep content fresh. Cache the response locally. The response includes everything the app needs to render the full UI.

Response

{
  "success": true,
  "sync_time": "2026-02-11T10:00:00+00:00",

  // ── Videos ──
  "videos": [
    {
      "id": 123,
      "vimeo_id": "987654321",
      "title": "Morning Yoga Flow",
      "description": "A gentle 10-minute morning stretch...",
      "duration": 600,
      "duration_formatted": "10:00",
      "thumbnail": "https://i.vimeocdn.com/video/...",
      "terms": [
        { "taxonomy": "workout_type", "slug": "yoga", "name": "Yoga" },
        { "taxonomy": "workout_duration", "slug": "10-min", "name": "10 min" }
      ],
      "box_type_ids": [1, 3],
      "equipment_ids": [5, 12],
      "equipment_items": [
        { "id": 5, "name": "Yoga Mat", "slug": "yoga-mat", "image": "https://..." }
      ],
      "rating": {
        "average": 4.5,
        "count": 12
      }
    }
  ],

  // ── Box Types (Equipment Trays) ──
  "box_types": [
    {
      "id": 1,
      "title": "Standard Tray",
      "slug": "standard-tray",
      "description": "Basic resistance training equipment",
      "video_ids": [123, 124, 125]
    }
  ],

  // ── Requestable Box Types (can be ordered to room) ──
  "requestable_box_types": [
    {
      "id": 3,
      "title": "Recovery Tray",
      "video_ids": [130, 131]
    }
  ],

  // ── Taxonomies (filters) ──
  "taxonomies": {
    "workout_type": [
      { "term_id": 5, "name": "Yoga", "slug": "yoga", "icon": "<svg>...</svg>" }
    ],
    "workout_duration": [
      { "term_id": 10, "name": "10 min", "slug": "10-min", "icon": "<svg>...</svg>" }
    ]
  },

  // ── Hotel Settings ──
  "settings": {
    "installed_box_type_ids": [1],
    "session_expiry_days": 7
  },

  // ── Branding (merged group + hotel overrides) ──
  "branding": {
    "logo_url": "https://...",
    "color_primary": "#FF5500",
    "color_secondary": "#333333",
    "font_heading": "system-ui",
    "font_body": "system-ui"
  },

  // ── Additional content ──
  "faqs": [{ "question": "...", "answer": "..." }],
  "help_content": "<p>HTML help content...</p>",
  "privacy_policy": "<p>Privacy policy text...</p>",
  "waiver": "<p>Fitness waiver text...</p>",
  "equipment": [/* requestable equipment items */],
  "box_equipment": [/* equipment included in trays */],
  "questions": [/* guest profile questions */],
  "content_blocks": {/* portal text customization */},
  "watermark": {/* video watermark settings */},
  "request_config": {/* equipment request form config */},
  "issue_config": {/* issue reporting config */}
}
GET /irf/v1/videos Auth Required

Returns videos assigned to the hotel's box types. Lighter than /sync when you only need video data.

Query Parameters

Response

{
  "success": true,
  "videos": [/* array of Video objects — see Data Models */],
  "total": 42
}
GET /irf/v1/box-types Auth Required

Returns box types (equipment trays) assigned to the hotel, with their video assignments.

Response

{
  "success": true,
  "box_types": [
    {
      "id": 1,
      "title": "Standard Tray",
      "slug": "standard-tray",
      "description": "Includes resistance bands and yoga mat",
      "video_ids": [123, 124, 125]
    }
  ]
}
tvOS: Understanding Box Types Box types are physical equipment trays placed in hotel rooms. Each tray has specific videos assigned to it. On the web, guests scan a QR code to "unlock" a tray and see its videos. On tvOS, the app will need to determine which trays are in the room via configuration or the settings.installed_box_type_ids field from the settings endpoint.
GET /irf/v1/taxonomies Auth Required

Returns taxonomy terms (workout types, durations) with SVG icons for UI filters.

Response

{
  "success": true,
  "taxonomies": {
    "workout_type": [
      {
        "term_id": 5,
        "name": "Yoga",
        "slug": "yoga",
        "icon": "<svg xmlns=...>...</svg>"
      },
      {
        "term_id": 6,
        "name": "HIIT",
        "slug": "hiit",
        "icon": "<svg xmlns=...>...</svg>"
      }
    ],
    "workout_duration": [
      {
        "term_id": 10,
        "name": "10 min",
        "slug": "10-min",
        "icon": "<svg xmlns=...>...</svg>"
      }
    ]
  }
}
SVG Icons The icon field contains raw SVG markup. On tvOS, you can render these as images or replace with native SF Symbols for a more native feel.
GET /irf/v1/settings Auth Required

Returns hotel-specific configuration settings.

Response

{
  "success": true,
  "settings": {
    "installed_box_type_ids": [1, 2],
    "session_expiry_days": 7
  }
}
tvOS: Default Box Types installed_box_type_ids is crucial for tvOS. These are the trays "pre-installed" at the hotel. On the web microsite, videos from these trays are automatically unlocked without needing a QR scan. On tvOS, you should treat these as the default accessible trays — all videos assigned to these box types should be playable.
GET /irf/v1/config Auth Required

Returns basic hotel identity information.

Response

{
  "success": true,
  "hotel": {
    "id": 1,
    "name": "The Grand Hotel",
    "slug": "the-grand-hotel"
  },
  "settings": { /* same as /settings */ }
}
GET /irf/v1/equipment Auth Required

Returns equipment items assigned to the hotel — both requestable items and items included in box types.

Response

{
  "success": true,
  "equipment": [
    {
      "id": 5,
      "name": "Yoga Mat",
      "slug": "yoga-mat",
      "description": "Premium non-slip yoga mat",
      "image_url": "https://...",
      "categories": ["flexibility"]
    }
  ],
  "box_equipment": [
    {
      "id": 5,
      "name": "Yoga Mat",
      "box_type_id": 1,
      "quantity": 1
    }
  ],
  "box_equipment_map": {
    "1": [5, 12, 15]
  }
}
GET /irf/v1/equipment/categories Auth Required

Returns equipment categories with hierarchical structure and SVG icons.

Response

{
  "success": true,
  "categories": [
    {
      "id": 1,
      "name": "Resistance",
      "slug": "resistance",
      "description": "Resistance training equipment",
      "parent_id": 0,
      "icon": "<svg>...</svg>"
    }
  ]
}

Analytics & Tracking Endpoints

POST /irf/v1/analytics Auth Required

Submit a batch of analytics events. The web microsite queues events locally and flushes them every 5 minutes via cron. The tvOS app can either batch events or use /track for real-time submission.

Request Body

{
  "events": [
    {
      "event_type": "play",
      "vimeo_id": "987654321",
      "tray_id": 1,
      "session_id": "tvos_abc123def456",
      "room_number": "101",
      "event_data": {
        "duration": 600,
        "position": 0
      },
      "user_agent": "IRF-tvOS/1.0 (tvOS 18.0; Apple TV 4K)"
    }
  ]
}

Event Parameters

Response

{
  "success": true,
  "recorded": 10,
  "total": 10,
  "errors": []
}
tvOS: Session ID Strategy Generate a UUID as the session ID. Consider resetting it daily or on a configurable interval (the web uses session_expiry_days from settings). If the room number is known, include it with every event so the hotel can correlate analytics with rooms.
POST /irf/v1/track Auth Required

Submit a single real-time analytics event. Use this for events that should appear immediately in the dashboard (e.g., play events), or as an alternative to batching.

Request Body

{
  "vimeo_id": "987654321",
  "event_type": "play",
  "session_id": "tvos_abc123def456",
  "data": {
    "duration": 600,
    "position": 0
  }
}
POST /irf/v1/heartbeat Auth Required

Report device health status. The Dashboard displays online/offline indicators for each hotel. Send a heartbeat every 5 minutes.

Request Body

{
  "site_url": "tvos://hotel-grand/device-001",
  "site_name": "The Grand Hotel - Room 101",
  "plugin_version": "1.0.0",
  "theme_version": "1.0.0",
  "wp_version": "tvOS 18.0",
  "php_version": "Swift 6.0"
}
Field Repurposing The heartbeat was designed for WordPress microsites. For tvOS, repurpose the fields: plugin_version → app version, wp_version → tvOS version, php_version → Swift version. We may add tvOS-specific fields in a future API version.

Guest Interaction Endpoints

POST /irf/v1/profile Auth Required

Submit guest demographic profile answers. Questions are configured per hotel in the Dashboard. The session ID links profile data to analytics for cross-analysis.

Request Body

{
  "session_id": "tvos_abc123def456",
  "answers": {
    "age_group": "26-35",
    "fitness_level": "intermediate",
    "workout_frequency": "3-5-times"
  }
}

The available questions and their options are returned in the /sync response under the questions key.

Response

{
  "success": true,
  "saved": 3,
  "total": 3,
  "errors": []
}
POST /irf/v1/rating Auth Required

Submit a 1-5 star rating for a video. One rating per session per video — resubmitting updates the existing rating.

Request Body

{
  "session_id": "tvos_abc123def456",
  "vimeo_id": "987654321",
  "rating": 5,
  "room_number": "101"
}

Response

{
  "success": true,
  "action": "created",    // or "updated" if re-rating
  "rating": 5,
  "average": 4.5,
  "count": 13
}
GET /irf/v1/ratings Auth Required

Get rating statistics for all videos or a specific video.

Query Parameters

Response

{
  "success": true,
  "videos": [
    {
      "vimeo_id": "987654321",
      "average": 4.5,
      "count": 13,
      "five_star": 8,
      "four_star": 3,
      "three_star": 1,
      "two_star": 1,
      "one_star": 0
    }
  ]
}
POST /irf/v1/equipment/request Auth Required  |  Rate Limited: 5/5min

Submit a guest request for equipment delivery to their room.

Request Body

{
  "session_id": "tvos_abc123def456",
  "item_id": 5,
  "item_name": "Yoga Mat",
  "request_type": "equipment",
  "form_data": {
    "room_number": "101",
    "guest_name": "John Doe",
    "notes": "Please deliver by 3pm"
  }
}

Response

{
  "success": true,
  "request_id": 42,
  "message": "Your request has been received..."
}
Request Form Configuration The form fields (which fields to show, which are required) are configured per hotel in the Dashboard and returned in the /sync response under request_config. If request_config.enabled is false, don't show the request UI.
POST /irf/v1/issue Auth Required  |  Rate Limited: 5/5min

Report an equipment or video issue.

Request Body

{
  "session_id": "tvos_abc123def456",
  "room_number": "101",
  "item_type": "equipment",    // "equipment" or "video"
  "item_id": 5,
  "item_name": "Yoga Mat",
  "issue_category": "damaged",
  "description": "Torn corner on the mat"
}

Issue Categories

Item TypeValid Categories
equipmentmissing, damaged, dirty, other
videowont_play, audio_issue, video_quality, wrong_content, other

Response

{
  "success": true,
  "issue_id": 42,
  "message": "Your issue has been reported. Thank you."
}

System Endpoints

GET /irf/v1/ping Public — No Auth

Public health check. Use to verify the API is reachable before attempting authenticated calls.

Response

{
  "success": true,
  "message": "IRF Dashboard API is running",
  "version": "2.5.0",
  "time": "2026-02-11T10:00:00+00:00"
}
GET /irf/v1/updates/check Auth Required

Check for available plugin/theme updates. Used by the WordPress microsite for remote deployment. Not directly relevant to tvOS but included for completeness.

Response

{
  "success": true,
  "updates": {
    "plugin": {
      "version": "2.5.6",
      "slug": "irf-microsite",
      "requires_wp": "6.0",
      "requires_php": "8.0"
    },
    "theme": {
      "version": "1.4.6"
    }
  }
}

Data Models

Video Object

{
  "id": 123,                              // Dashboard video ID (integer)
  "vimeo_id": "987654321",               // Vimeo video ID (string)
  "title": "Morning Yoga Flow",           // Display title
  "description": "A gentle 10-min...",     // Full description
  "duration": 600,                         // Duration in seconds (integer)
  "duration_formatted": "10:00",          // Human-readable (string)
  "thumbnail": "https://i.vimeocdn...",   // Vimeo thumbnail URL
  "terms": [                                // Taxonomy assignments
    {
      "taxonomy": "workout_type",
      "slug": "yoga",
      "name": "Yoga"
    }
  ],
  "box_type_ids": [1, 3],                 // Which trays unlock this video
  "equipment_ids": [5, 12],               // Equipment used in video
  "equipment_items": [                      // Full equipment details
    {
      "id": 5,
      "name": "Yoga Mat",
      "slug": "yoga-mat",
      "description": "...",
      "image": "https://..."
    }
  ],
  "rating": {                               // Aggregate rating
    "average": 4.5,
    "count": 12
  }
}
Video Playback Videos are hosted on Vimeo. Use the vimeo_id with the Vimeo Player SDK for tvOS to embed playback. The SDK handles DRM, adaptive bitrate, and Vimeo's CDN.

Box Type Object

{
  "id": 1,                                // Box type ID (integer)
  "title": "Standard Tray",               // Display name
  "slug": "standard-tray",               // URL-safe slug
  "description": "Resistance bands...",   // Description
  "video_ids": [123, 124, 125]            // Videos assigned to this tray
}

Equipment Object

{
  "id": 5,
  "name": "Yoga Mat",
  "slug": "yoga-mat",
  "description": "Premium non-slip yoga mat",
  "image_url": "https://...",
  "categories": ["flexibility"]
}

Taxonomy Term Object

{
  "term_id": 5,
  "name": "Yoga",
  "slug": "yoga",
  "icon": "<svg xmlns=...>...</svg>"   // Raw SVG markup
}

Branding Object

{
  "logo_url": "https://...",               // Hotel/group logo
  "color_primary": "#FF5500",              // Primary brand color
  "color_secondary": "#333333",            // Secondary color
  "font_heading": "system-ui",             // Heading font family
  "font_body": "system-ui"                 // Body font family
}
Branding Hierarchy Hotels belong to a Hotel Group (e.g., a hotel chain). Branding is inherited from the group and can be overridden per hotel. The /sync response returns the merged result — you don't need to handle the merge logic.

Video Access Logic

Understanding how video access works is essential for the tvOS implementation.

Core Concept

Videos are locked by default. A video is only accessible when the guest has access to at least one of the video's assigned box types (trays). Each video has a box_type_ids array listing which trays unlock it.

Access Sources

SourceDescriptionRelevant to tvOS?
Default Box Types Pre-installed trays (settings.installed_box_type_ids) — automatically unlocked Yes — primary access method
QR Scanned Trays Guest scans QR code on a physical tray to unlock it No — no QR scanning on Apple TV

Access Check Algorithm

// Swift pseudocode for tvOS
func isVideoAccessible(video: Video, unlockedTrayIDs: [Int]) -> Bool {
    // No unlocked trays = no access
    guard !unlockedTrayIDs.isEmpty else { return false }

    // Video must be assigned to at least one unlocked tray
    guard !video.boxTypeIDs.isEmpty else { return false }

    return video.boxTypeIDs.contains(where: { unlockedTrayIDs.contains($0) })
}

// For tvOS, "unlocked trays" = settings.installed_box_type_ids
// (plus any additional logic based on room/device mapping)
let unlockedTrays = settings.installedBoxTypeIDs
let accessibleVideos = allVideos.filter { isVideoAccessible($0, unlockedTrays) }
Type Safety The box_type_ids array in the video object contains integers. Ensure strict type matching — 1 (integer) must match 1 (integer), not "1" (string). The web microsite had a bug where PHP returned string IDs that didn't match JavaScript integers with strict equality.

Session Management

Sessions are anonymous and used to correlate analytics, ratings, and profile data for the same guest.

Web Microsite Session

tvOS Session (Recommended Approach)

Event Types Reference

EventDescriptionRequires Video IDtvOS Priority
qr_scanGuest scanned QR codeNoN/A
page_viewGeneral page/screen viewNoOptional
workout_viewVideo detail screen viewedYesRecommended
loadVideo player loadedYesRecommended
playVideo playback startedYesRequired
pauseVideo pausedYesRecommended
seekUser seeked in videoYesOptional
progressWatch progress updateYesOptional
completeVideo watched to endYesRequired
milestone_10Reached 10% watchedYesRecommended
milestone_25Reached 25% watchedYesRecommended
milestone_50Reached 50% watchedYesRecommended
milestone_75Reached 75% watchedYesRecommended
milestone_90Reached 90% watchedYesRecommended
milestone_100Reached 100% watchedYesRecommended
Milestone Tracking Milestones build a retention curve in the Dashboard analytics. Fire each milestone event once per video playback session when the viewer passes that threshold. Calculate as: (currentPosition / totalDuration) * 100. The milestone events should include the vimeo_id and session_id.

Database Schema Overview

The Dashboard maintains these key tables. Understanding the schema helps when interpreting API responses.

TableDescriptionAPI Exposure
irf_hotelsHotel properties with API keys, branding, settingsVia auth scope
irf_hotel_groupsHotel chains/brands — branding inheritanceMerged into branding
irf_box_typesEquipment tray definitions/box-types, /sync
irf_hotel_box_typesHotel ↔ tray assignments with type (installed/requestable)/settings, /sync
irf_box_type_videosTray ↔ video assignmentsAs video_ids in box types
wp_posts (irf_video)Video content (title, description, Vimeo ID)/videos, /sync
irf_equipmentEquipment items/equipment, /sync
irf_analyticsRaw analytics events (90-day retention)Write via /analytics
irf_analytics_hourlyAggregated analytics (permanent)Dashboard charts
irf_video_ratings1-5 star ratings per session per video/rating, /ratings
irf_session_profilesGuest demographic answersWrite via /profile
irf_equipment_requestsEquipment delivery requests/equipment/request
irf_issuesGuest issue reports/issue
irf_faqsFAQ content/sync
irf_questionsProfile question definitions/sync

tvOS Implementation Guide

Recommended App Architecture

// Suggested data flow for tvOS app

1. App Launch
   ├── GET /ping         → Verify API reachable
   ├── GET /sync         → Fetch all content (cache locally)
   └── POST /heartbeat   → Report device online

2. Content Display
   ├── Filter videos by installed_box_type_ids
   ├── Group by workout_type taxonomy
   ├── Show thumbnails, durations, ratings
   └── Apply branding (colors, logo)

3. Video Playback (via Vimeo Player SDK)
   ├── POST /track { event_type: "play" }
   ├── Track milestones at 10/25/50/75/90/100%
   ├── POST /track { event_type: "complete" }
   └── Prompt for rating after completion

4. Background Tasks
   ├── Heartbeat every 5 minutes
   ├── Resync content every 12 hours
   └── Flush queued analytics events

API Key Configuration

Deployment Model

Each hotel gets one API key from the Dashboard. All Apple TV devices in that hotel share the same key. The key identifies the hotel — the Dashboard scopes all data and analytics to that hotel automatically.

The app should provide a setup screen (perhaps accessible via a settings menu or initial configuration) where the API key and room number can be entered. Room number can be hardcoded per device during installation or entered by staff.

Vimeo Player Integration

Videos are hosted on Vimeo with unlisted/private access. Use the Vimeo Player SDK for iOS/tvOS with the vimeo_id from the API response. The SDK handles:

Caching Strategy

DataCache DurationRefresh Trigger
Full sync response12 hoursApp launch, manual refresh
Video thumbnailsUntil sync changesURL change in sync
Branding/logoUntil sync changesURL change in sync
Analytics eventsQueue locallyFlush every 5 minutes

IRF API Documentation — Version 1.0

Last updated: February 2026

Base URL: https://inroomfitness.com/wp-json/irf/v1/

Questions? Contact the IRF development team.