Overview
/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:
- Central Dashboard — Admin site managing all hotels, videos, equipment, and analytics
- Hotel Microsites — One WordPress site per hotel, syncs content from the Dashboard via API
- tvOS App (planned) — Native Apple TV client replacing the microsite for in-room TV systems
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
Data Flow Summary
| Direction | Data | Used 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
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
| Scope | Limit |
|---|---|
| Per API Key | 100 requests / minute |
| Per IP Address | 1,000 requests / hour |
| Equipment Requests | 5 requests / IP / 5 minutes |
| Issue Reports | 5 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 Status | Code | Description |
|---|---|---|
| 400 | bad_request | Missing or invalid parameters |
| 401 | unauthorized | Missing or invalid API key |
| 403 | forbidden | IP not in whitelist / insufficient permissions |
| 404 | not_found | Resource not found |
| 429 | rate_limited | Too many requests |
| 500 | server_error | Internal server error |
tvOS Considerations
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)
- Guest scans QR code on equipment tray → opens
hotel.site/?tray={box_type_id} - Box type ID stored in browser localStorage
- Videos filtered: only videos matching unlocked box type IDs are playable
- Hotel has "default" box type IDs (pre-unlocked without QR scan)
Proposed tvOS Flow (To Be Confirmed)
- Apple TV device is configured per hotel with the hotel's API key
- Device may pass a room number or room type to identify which trays are in the room
- The API returns all videos for the hotel — the app determines access based on box types assigned
- Alternatively, the hotel's "default" box type IDs (configured in the Dashboard) pre-unlock all standard content
- One API key per hotel, or one per room/device? Currently: one per hotel (recommended)
- How does the app know which trays are in the room? Room number mapping? Or show all videos?
- Does the hotel want per-room content control, or same content for all rooms?
- Should the app identify itself as a specific device (for per-device analytics)?
What the tvOS App Needs at Minimum
| Endpoint | Purpose | Priority |
|---|---|---|
GET /sync | Fetch all videos, box types, branding, settings | Required |
GET /videos | Fetch videos (lighter than full sync) | Required |
GET /box-types | Understand which trays unlock which videos | Required |
GET /taxonomies | Category/duration filters with icons | Required |
GET /settings | Get default box types, session config | Required |
POST /analytics | Report video play/completion events | Required |
POST /track | Real-time event tracking | Recommended |
POST /heartbeat | Report device online status | Recommended |
POST /rating | Video ratings (1-5 stars) | Optional |
GET /equipment | Equipment list (if showing equipment info) | Optional |
POST /issue | Report problems | Optional |
Content Endpoints
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.
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 */}
}
Returns videos assigned to the hotel's box types. Lighter than /sync when you only need video data.
Query Parameters
-
group
string
optional
Filter by video group slug (e.g.,
premium,standard) -
category
string
optional
Filter by category slug (e.g.,
yoga,hiit)
Response
{
"success": true,
"videos": [/* array of Video objects — see Data Models */],
"total": 42
}
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]
}
]
}
settings.installed_box_type_ids field from the settings endpoint.
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>"
}
]
}
}
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.
Returns hotel-specific configuration settings.
Response
{
"success": true,
"settings": {
"installed_box_type_ids": [1, 2],
"session_expiry_days": 7
}
}
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.
Returns basic hotel identity information.
Response
{
"success": true,
"hotel": {
"id": 1,
"name": "The Grand Hotel",
"slug": "the-grand-hotel"
},
"settings": { /* same as /settings */ }
}
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]
}
}
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
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
-
event_type
string
required
Event type — see Event Types Reference
-
vimeo_id
string
required for video events
Vimeo video ID (not the IRF video ID)
-
tray_id
integer
optional
Box type ID of the tray being used
-
session_id
string
required
Anonymous session identifier. Generate a unique ID per guest session (e.g., UUID). Used to link analytics, ratings, and profile data.
-
room_number
string
optional
Hotel room number
-
event_data
object
optional
Additional event context (position in seconds, duration, etc.)
-
user_ip
string
optional
Client IP (server will capture if not provided)
-
user_agent
string
optional
Device user agent string
Response
{
"success": true,
"recorded": 10,
"total": 10,
"errors": []
}
session_expiry_days from settings). If the room number is known, include it with every event so the hotel can correlate analytics with rooms.
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
}
}
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"
}
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
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": []
}
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 rating statistics for all videos or a specific video.
Query Parameters
-
vimeo_id
string
optional
Filter to a specific video. Omit to get all video ratings.
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
}
]
}
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..."
}
/sync response under request_config. If request_config.enabled is false, don't show the request UI.
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 Type | Valid Categories |
|---|---|
equipment | missing, damaged, dirty, other |
video | wont_play, audio_issue, video_quality, wrong_content, other |
Response
{
"success": true,
"issue_id": 42,
"message": "Your issue has been reported. Thank you."
}
System Endpoints
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"
}
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
}
}
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
}
/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
| Source | Description | Relevant 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) }
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
- Stored in
localStorage+ cookie - Contains:
sessionId,unlockedTrays,startTime,roomNumber - Expires after
session_expiry_days(default: 7) - Created on first page load
tvOS Session (Recommended Approach)
- Generate a UUID per session
- Store in UserDefaults or app storage
- Reset daily (hotel guests typically stay 1-3 nights) or per
session_expiry_days - Include
session_idin all analytics, rating, and profile submissions - If room number is known, include it with every event for room-level analytics
Event Types Reference
| Event | Description | Requires Video ID | tvOS Priority |
|---|---|---|---|
qr_scan | Guest scanned QR code | No | N/A |
page_view | General page/screen view | No | Optional |
workout_view | Video detail screen viewed | Yes | Recommended |
load | Video player loaded | Yes | Recommended |
play | Video playback started | Yes | Required |
pause | Video paused | Yes | Recommended |
seek | User seeked in video | Yes | Optional |
progress | Watch progress update | Yes | Optional |
complete | Video watched to end | Yes | Required |
milestone_10 | Reached 10% watched | Yes | Recommended |
milestone_25 | Reached 25% watched | Yes | Recommended |
milestone_50 | Reached 50% watched | Yes | Recommended |
milestone_75 | Reached 75% watched | Yes | Recommended |
milestone_90 | Reached 90% watched | Yes | Recommended |
milestone_100 | Reached 100% watched | Yes | Recommended |
(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.
| Table | Description | API Exposure |
|---|---|---|
irf_hotels | Hotel properties with API keys, branding, settings | Via auth scope |
irf_hotel_groups | Hotel chains/brands — branding inheritance | Merged into branding |
irf_box_types | Equipment tray definitions | /box-types, /sync |
irf_hotel_box_types | Hotel ↔ tray assignments with type (installed/requestable) | /settings, /sync |
irf_box_type_videos | Tray ↔ video assignments | As video_ids in box types |
wp_posts (irf_video) | Video content (title, description, Vimeo ID) | /videos, /sync |
irf_equipment | Equipment items | /equipment, /sync |
irf_analytics | Raw analytics events (90-day retention) | Write via /analytics |
irf_analytics_hourly | Aggregated analytics (permanent) | Dashboard charts |
irf_video_ratings | 1-5 star ratings per session per video | /rating, /ratings |
irf_session_profiles | Guest demographic answers | Write via /profile |
irf_equipment_requests | Equipment delivery requests | /equipment/request |
irf_issues | Guest issue reports | /issue |
irf_faqs | FAQ content | /sync |
irf_questions | Profile 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
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:
- Adaptive bitrate streaming
- DRM protection
- Playback controls
- Progress tracking callbacks (for milestone events)
Caching Strategy
| Data | Cache Duration | Refresh Trigger |
|---|---|---|
| Full sync response | 12 hours | App launch, manual refresh |
| Video thumbnails | Until sync changes | URL change in sync |
| Branding/logo | Until sync changes | URL change in sync |
| Analytics events | Queue locally | Flush 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.