REST API
Overview
The OpenWatchParty plugin exposes REST API endpoints through Jellyfin’s web server.
Base URL: http(s)://<jellyfin-host>:<port>/OpenWatchParty
Endpoints
GET /OpenWatchParty/ClientScript
Serves the client JavaScript bundle.
Authentication: None required
Response:
- Content-Type:
text/javascript - Cache-Control:
public, max-age=31536000, immutablewhen thevquery value matches the current client hash (the URL the plugin injects into Jellyfin),no-storewhenvis any other value, andpublic, max-age=3600withoutv. Module responses under/OpenWatchParty/Client/follow the same rule. - ETag: Hash-based cache validation
Headers:
| Header | Description |
|——–|————-|
| If-None-Match | Send cached ETag for validation |
Status Codes: | Code | Description | |——|————-| | 200 | JavaScript content returned | | 304 | Not Modified (cache valid) | | 404 | Script resource not found |
Example:
# First request
curl -i "http://localhost:8096/OpenWatchParty/ClientScript"
# Subsequent request with caching
curl -i -H "If-None-Match: \"abc123...\"" \
"http://localhost:8096/OpenWatchParty/ClientScript"
GET /OpenWatchParty/Token
Generates a JWT token for the authenticated user.
Authentication: Required (Jellyfin auth)
Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"auth_enabled": true,
"expires_in": 3600,
"user_id": "abc123",
"user_name": "John",
"session_server_url": "wss://jellyfin.example.com/ws",
"hide_native_syncplay_button": false,
"show_sync_nudge_button": false,
"appearance_opacity": 55,
"appearance_text_size": 14,
"invite_ttl_seconds": 3600,
"protocol_version": 1
}
When insecure development is explicitly enabled:
{
"token": null,
"auth_enabled": false,
"insecure_mode": true,
"user_id": "abc123",
"user_name": "John",
"session_server_url": "wss://jellyfin.example.com/ws",
"hide_native_syncplay_button": false,
"show_sync_nudge_button": false,
"appearance_opacity": 55,
"appearance_text_size": 14,
"invite_ttl_seconds": 3600,
"protocol_version": 1
}
hide_native_syncplay_button is true when the administrator enabled Hide Jellyfin’s SyncPlay button; the web client then hides the built-in SyncPlay button, and shows it again when a later response turns the setting off.
show_sync_nudge_button is true when the administrator enabled Show the sync adjustment button in rooms; guests then get the sync adjustment in the room bar. Only an explicit true turns it on.
appearance_opacity (40-100, in steps of 5) and appearance_text_size (12-18) are the Room bar opacity in percent and the Text size in pixels. The web client applies them to the room bar, its drop-downs, the lobby panel, the up-next strip and the toasts, and uses the defaults (55 and 14) for a missing or out-of-range value.
invite_ttl_seconds is the configured Invite TTL (60-86400 seconds). The client sends it when asking the session server for a new invite link; the session server clamps it again.
protocol_version is the WebSocket protocol version the issued token authorizes; it is carried in the client’s auth message (see the protocol specification).
Status Codes: | Code | Description | |——|————-| | 200 | Token generated successfully | | 401 | Not authenticated or claims missing | | 429 | Rate limit exceeded (30 tokens/min) | | 500 | Plugin not configured | | 503 | Invalid or missing session destination, automatic discovery not opted into, or JWT signing configuration missing or invalid |
Rate Limiting:
- JWT issuance is limited to 30 tokens per fixed one-minute window per user
- The window starts with the first accepted request; the insecure no-token response does not consume a token slot
Example:
# With Jellyfin API key
curl -H "Authorization: MediaBrowser Token=\"YOUR_API_KEY\"" \
"http://localhost:8096/OpenWatchParty/Token"
# Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"auth_enabled": true,
"expires_in": 3600,
"user_id": "d4f8a2b1-c3e4-5f6a-7b8c-9d0e1f2a3b4c",
"user_name": "admin",
"session_server_url": "wss://party.example.com/ws",
"hide_native_syncplay_button": false,
"show_sync_nudge_button": false,
"appearance_opacity": 55,
"appearance_text_size": 14,
"invite_ttl_seconds": 3600,
"protocol_version": 1
}
JWT Token Structure
When authentication is enabled, the generated token contains:
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "user-uuid",
"name": "username",
"aud": "OpenWatchParty",
"iss": "Jellyfin",
"iat": 1678900000,
"exp": 1678903600,
"jti": "unique-token-id"
}
The default signer is HS256 using the configured shared secret. An activated
plugin-managed RSA signing key instead issues RS256 tokens; the session server
can validate them using its configured public-key trust store. aud and iss
are string claims, and jti is generated per token.
Claims:
| Claim | Description |
|——-|————-|
| sub | Subject (user ID) |
| name | User display name |
| aud | Audience (configurable) |
| iss | Issuer (configurable) |
| iat | Issued at timestamp |
| exp | Expiration timestamp |
| jti | Unique token identifier |
Error Responses
All error responses follow this format:
{
"error": "Error description"
}
Common Errors:
| Status | Error | Description |
|---|---|---|
| 401 | “User identity not found in claims” | Jellyfin auth failed or claims missing |
| 429 | “Rate limit exceeded. Try again later.” | Too many token requests |
| 500 | “Plugin not configured” | Plugin configuration issue |
| 503 | Configuration error | Invalid session URL, automatic discovery not opted into, or unavailable/invalid JWT signing configuration |
Using the API
From JavaScript
// Get token
async function getToken() {
const response = await fetch('/OpenWatchParty/Token', {
credentials: 'include' // Include Jellyfin auth cookies
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
return await response.json();
}
// Load client script
function loadClientScript() {
const script = document.createElement('script');
script.src = '/OpenWatchParty/ClientScript';
document.head.appendChild(script);
}
From External Applications
# Authenticate with Jellyfin first
TOKEN=$(curl -s -X POST "http://localhost:8096/Users/AuthenticateByName" \
-H "X-Emby-Authorization: MediaBrowser Client=\"App\", Device=\"CLI\", DeviceId=\"abc\", Version=\"1.0\"" \
-H "Content-Type: application/json" \
-d '{"Username":"admin","Pw":"password"}' | jq -r '.AccessToken')
# Get OpenWatchParty token
curl -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
"http://localhost:8096/OpenWatchParty/Token"
Plugin information, diagnostics and signing key
GET /OpenWatchParty/Info returns the plugin ID, name and version for the
configuration UI.
The diagnostics routes require administrator elevation:
GET /OpenWatchParty/Diagnostics/Statusreturns redacted readiness checks and plugin, Jellyfin, ABI and protocol version details.POST /OpenWatchParty/Diagnostics/Runrepeats the checks and attempts a session-server/healthrequest with a five-second timeout. A failed check is reported in the response rather than changing the plugin configuration.
The RSA signing-key routes also require administrator elevation:
GET /OpenWatchParty/SigningKeyreturns the public JWK, issuer and whether the plugin-managed key is active. It creates the key if one does not exist.POST /OpenWatchParty/SigningKey/Activateactivates the key matching the request’skidand returns204; a mismatch returns409.
Configuration API
Plugin configuration is managed through Jellyfin’s standard plugin configuration API:
GET /System/Configuration/Plugin/{pluginId}
Get plugin configuration.
curl -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
"http://localhost:8096/System/Configuration/Plugin/0f2fd0fd-09ff-4f49-9f1c-4a8f421a4b7d"
POST /System/Configuration/Plugin/{pluginId}
Update plugin configuration.
curl -X POST \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
-H "Content-Type: application/json" \
-d '{
"JwtSecret": "<output-of-openssl-rand-base64-32>",
"JwtAudience": "OpenWatchParty",
"JwtIssuer": "Jellyfin",
"TokenTtlSeconds": 3600,
"InviteTtlSeconds": 3600,
"SessionServerUrl": "wss://party.example.com/ws",
"AllowAutoDetectedSessionServer": false,
"HideNativeSyncPlayButton": false,
"ShowSyncNudgeButton": false,
"AppearanceOpacity": 55,
"AppearanceTextSize": 14
}' \
"http://localhost:8096/System/Configuration/Plugin/0f2fd0fd-09ff-4f49-9f1c-4a8f421a4b7d"
Note: Requires admin privileges.
Configuration Fields Reference
| Field | Type | Default | Description |
|---|---|---|---|
JwtSecret |
string | "" |
Base64/Base64URL secret generated from at least 32 random bytes. Required unless AllowInsecureNoAuth is explicitly enabled. |
AllowInsecureNoAuth |
bool | false |
Explicit local-development override. Never enable in production. |
JwtAudience |
string | "OpenWatchParty" |
The aud (audience) claim in generated tokens. Must match the session server’s expected audience if configured. |
JwtIssuer |
string | "Jellyfin" |
The iss (issuer) claim in generated tokens. Must match the session server’s expected issuer if configured. |
TokenTtlSeconds |
int | 3600 |
Token lifetime in seconds. Valid range: 60-86400 (1 min to 24 hours). Values outside this range are clamped. |
InviteTtlSeconds |
int | 3600 |
Lifetime of room invite links in seconds. Valid range: 60-86400 (1 min to 24 hours). Values outside this range are clamped. The session server enforces the same bounds when minting a ticket. |
SessionServerUrl |
string | "" |
Absolute WebSocket URL (for example, wss://party.example.com/ws). An empty value requires the auto-detection opt-in below. |
AllowAutoDetectedSessionServer |
bool | false |
Explicitly permits same-host, port-3000 auto-detection when SessionServerUrl is empty. Without it, token issuance returns HTTP 503. |
HideNativeSyncPlayButton |
bool | false |
Hides Jellyfin’s built-in SyncPlay controls in Jellyfin Web headers. |
ShowSyncNudgeButton |
bool | false |
Gives guests a local sync-adjustment control; it does not issue a room seek. |
AppearanceOpacity |
int | 55 |
Opacity from 40 to 100, rounded to steps of 5. Applies to the room bar, drop-downs, lobby panel, up-next strip and toasts. |
AppearanceTextSize |
int | 14 |
Base text size from 12 to 18 px for those surfaces; icons and spacing scale with it. |
SessionServerUrl Behavior
The SessionServerUrl field determines how clients connect to the session server. A configured URL must be absolute ws:// or wss://, include a host, and have no credentials, query string or fragment. When it is empty, AllowAutoDetectedSessionServer must be true for token issuance to succeed:
| Value | Behavior |
|---|---|
Empty string "", opt-in enabled |
Auto-detect: uses current hostname with port 3000. Protocol matches page (http→ws, https→wss). Example: browsing https://jellyfin.example.com connects to wss://jellyfin.example.com:3000/ws |
| Full URL | Uses the validated URL after URI normalization. The session server endpoint normally ends in /ws. Example: wss://party.example.com/ws |
When to set explicitly:
- Session server runs on a different host
- Using a reverse proxy that routes both
/wsand/inviteto the session server - Port 3000 is not accessible from clients
Session Server API
Besides /health, the session server exposes /ready and /metrics for operators (see the Monitoring guide) and one endpoint for clients.
POST /invite
Mints a short-lived, room-scoped invite ticket for a watch party host.
Authentication: Required (session JWT from GET /OpenWatchParty/Token, sent as Authorization: Bearer <token>)
Request:
{
"room_id": "uuid-room-id",
"ttl_seconds": 3600
}
| Field | Type | Required | Description |
|---|---|---|---|
room_id |
string | Yes | Room the ticket is scoped to; the authenticated user must be its host |
ttl_seconds |
number | No | Requested lifetime; clamped to 60-86400 seconds (default 3600) |
Response (200):
{
"ticket": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": 1678903600
}
Status Codes: | Code | Description | |——|————-| | 200 | Ticket minted | | 401 | Missing or invalid session JWT | | 403 | The authenticated user is not the room host | | 404 | Room not found (closed rooms included) | | 429 | Rate limit exceeded (10 requests/min per user) | | 503 | Invite links require JWT authentication with a shared secret |
The link never contains the Jellyfin API token or the session JWT: the browser-side ticket is scoped to one room, expires, and only ever appears in the invite URL (?owp_invite=<ticket>).
WebSocket API
The session server uses WebSocket for real-time communication:
Endpoint: ws(s)://<host>:3000/ws
See Protocol Documentation for the complete WebSocket message specification.