Before You Start
You’ll need a TMDB ID for the content you want to stream. Every movie and TV show on themoviedb.org has one — it’s the number in the URL.
URL TMDB ID Title themoviedb.org/movie/550550Fight Club themoviedb.org/movie/2720527205Inception themoviedb.org/tv/13961396Breaking Bad themoviedb.org/tv/9499794997House of the Dragon
Authenticate
The Vyla API supports two authentication methods — session tokens for browser apps, and API keys for server-side integrations. See Authentication for full details.
Browser apps: your server calls POST /api/auth with a valid API key to receive a session token, then passes that token to the client. The client sends X-Session-Token on every request. Your API key never touches the browser.
Server-side: send a standard or partner key as Authorization: Bearer <key> or X-API-Key: <key>.
The public key (public_api_key) can only access /api/health, /api/subtitles, and /api/downloads. Streaming endpoints (/movie, /tv, /api/test) require a standard or partner key, or a session token issued from one via POST /api/auth. POST /api/auth itself requires a valid API key of any tier.
Step 1 — Open the SSE Stream
The /movie and /tv endpoints are Server-Sent Events — not JSON. Open a streaming connection and handle three event types as they arrive: meta, source, and done.
Movie (session token)
TV Episode (session token)
TOKEN = $( curl -s -X POST https://1c34-y.hf.space/api/auth \
-H "Authorization: Bearer YOUR_API_KEY" | jq -r .token )
curl -N "https://1c34-y.hf.space/movie?id=550" \
-H "X-Session-Token: $TOKEN "
Example SSE stream output
data: {"type":"meta","meta":{"id":550,"title":"Fight Club","release_date":"1999-10-15","runtime":139},"subtitles":[{"label":"English","file":"https://sub.vdrk.site/v1/vtt/movie/550/English.vtt","type":"vtt","source":"v1"},{"label":"Spanish","file":"https://sub.vdrk.site/v1/vtt/movie/550/Spanish.vtt","type":"vtt","source":"v1"}]}
data: {"type":"source","source":{"source":"provider-a","label":"Provider A","url":"https://1c34-y.hf.space/api?url=...&pa=1"}}
data: {"type":"source","source":{"source":"provider-b","label":"Provider B","url":"https://1c34-y.hf.space/api?url=...&pb=1"}}
data: {"type":"done","total":2}
Step 2 — Handle Events and Play
Parse each data: line as JSON and act on the type field. Start playback on the first source event — don’t wait for done.
<! DOCTYPE html >
< html >
< head >
< script src = "https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js" ></ script >
</ head >
< body >
< video id = "player" controls style = "width:100%" ></ video >
< div id = "status" > Connecting… </ div >
< script >
const BASE = 'https://1c34-y.hf.space' ;
const video = document . getElementById ( 'player' );
const status = document . getElementById ( 'status' );
let hls ;
let sources = [];
let started = false ;
function playSource ( url ) {
hls ?. destroy ();
if ( ! Hls . isSupported ()) { video . src = url ; return ; }
hls = new Hls ();
hls . loadSource ( url );
hls . attachMedia ( video );
hls . on ( Hls . Events . MANIFEST_PARSED , () => {
status . textContent = 'Playing' ;
video . play ();
});
hls . on ( Hls . Events . ERROR , ( _ , err ) => {
if ( err . fatal ) {
const next = sources . find ( s => s . url !== url );
if ( next ) { status . textContent = `Trying ${ next . label } …` ; playSource ( next . url ); }
else status . textContent = 'All sources failed.' ;
}
});
}
async function loadMovie ( tmdbId ) {
const { token } = await fetch ( ` ${ BASE } /api/auth` , {
method: 'POST' ,
headers: { 'Authorization' : 'Bearer YOUR_API_KEY' }
}). then ( r => r . json ());
const res = await fetch ( ` ${ BASE } /movie?id= ${ tmdbId } ` , {
headers: { 'X-Session-Token' : token }
});
const reader = res . body . getReader ();
const decoder = new TextDecoder ();
let buffer = '' ;
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
buffer += decoder . decode ( value , { stream: true });
const lines = buffer . split ( ' \n ' );
buffer = lines . pop ();
for ( const line of lines ) {
if ( ! line . startsWith ( 'data: ' )) continue ;
const event = JSON . parse ( line . slice ( 6 ));
if ( event . type === 'meta' ) {
status . textContent = `Loading ${ event . meta ?. title ?? '' } …` ;
event . subtitles ?. forEach (( sub , i ) => {
const track = document . createElement ( 'track' );
track . kind = 'subtitles' ;
track . label = sub . label ;
track . src = sub . file ;
track . default = i === 0 ;
video . appendChild ( track );
});
}
if ( event . type === 'source' ) {
sources . push ( event . source );
if ( ! started ) { started = true ; playSource ( event . source . url ); }
}
if ( event . type === 'done' && ! started ) {
status . textContent = 'No sources available.' ;
}
}
}
}
loadMovie ( 550 );
</ script >
</ body >
</ html >
import { useEffect , useRef , useState } from 'react' ;
import Hls from 'hls.js' ;
const BASE = 'https://1c34-y.hf.space' ;
export function Player ({ tmdbId } : { tmdbId : number }) {
const videoRef = useRef < HTMLVideoElement >( null );
const hlsRef = useRef < Hls | null >( null );
const sourcesRef = useRef < string []>([]);
const [ status , setStatus ] = useState ( 'Connecting…' );
function playUrl ( url : string ) {
if ( ! videoRef . current ) return ;
hlsRef . current ?. destroy ();
if ( ! Hls . isSupported ()) { videoRef . current . src = url ; return ; }
const hls = new Hls ();
hlsRef . current = hls ;
hls . loadSource ( url );
hls . attachMedia ( videoRef . current );
hls . on ( Hls . Events . MANIFEST_PARSED , () => {
setStatus ( 'Playing' );
videoRef . current ?. play ();
});
hls . on ( Hls . Events . ERROR , ( _ , err ) => {
if ( err . fatal ) {
const next = sourcesRef . current . find ( u => u !== url );
if ( next ) { setStatus ( 'Trying next source…' ); playUrl ( next ); }
else setStatus ( 'All sources failed.' );
}
});
}
useEffect (() => {
let started = false ;
( async () => {
const { token } = await fetch ( ` ${ BASE } /api/auth` , {
method: 'POST' ,
headers: { 'Authorization' : 'Bearer YOUR_API_KEY' }
}). then ( r => r . json ());
const res = await fetch ( ` ${ BASE } /movie?id= ${ tmdbId } ` , {
headers: { 'X-Session-Token' : token }
});
const reader = res . body ! . getReader ();
const decoder = new TextDecoder ();
let buffer = '' ;
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
buffer += decoder . decode ( value , { stream: true });
const lines = buffer . split ( ' \n ' );
buffer = lines . pop () ! ;
for ( const line of lines ) {
if ( ! line . startsWith ( 'data: ' )) continue ;
const event = JSON . parse ( line . slice ( 6 ));
if ( event . type === 'source' ) {
sourcesRef . current . push ( event . source . url );
if ( ! started ) { started = true ; playUrl ( event . source . url ); }
}
if ( event . type === 'done' && ! started ) setStatus ( 'No sources available.' );
}
}
})();
return () => hlsRef . current ?. destroy ();
}, [ tmdbId ]);
return (
<>
< video ref = { videoRef } controls style = { { width: '100%' } } />
< p > { status } </ p >
</>
);
}
import requests, json
BASE = 'https://1c34-y.hf.space'
def stream_movie ( tmdb_id : int , api_key : str ):
sources = []
subtitles = []
with requests.get(
f ' { BASE } /movie' ,
params = { 'id' : tmdb_id},
headers = { 'Authorization' : f 'Bearer { api_key } ' },
stream = True
) as res:
for line in res.iter_lines():
if not line or not line.startswith( b 'data: ' ):
continue
event = json.loads(line[ 6 :])
if event[ 'type' ] == 'meta' :
subtitles = event.get( 'subtitles' , [])
print ( 'Title:' , event[ 'meta' ].get( 'title' ))
elif event[ 'type' ] == 'source' :
sources.append(event[ 'source' ])
print ( f " + { event[ 'source' ][ 'label' ] } : { event[ 'source' ][ 'url' ] } " )
elif event[ 'type' ] == 'done' :
print ( f "Done. { event[ 'total' ] } sources." )
break
return sources, subtitles
sources, subtitles = stream_movie( 550 , 'your_standard_api_key' )
Step 3 — Build a Fallback Queue
Sources stream in order of speed. Load the first one immediately and queue the rest — if one dies mid-stream, try the next.
const BASE = 'https://1c34-y.hf.space' ;
async function streamWithFallback (
videoEl : HTMLVideoElement ,
tmdbId : number ,
season ?: number ,
episode ?: number
) {
const { token } = await fetch ( ` ${ BASE } /api/auth` , {
method: 'POST' ,
headers: { 'Authorization' : 'Bearer YOUR_API_KEY' }
}). then ( r => r . json ());
const endpoint = season && episode
? ` ${ BASE } /tv?id= ${ tmdbId } &season= ${ season } &episode= ${ episode } `
: ` ${ BASE } /movie?id= ${ tmdbId } ` ;
const queue : string [] = [];
let started = false ;
let hls : any ;
function tryNext () {
if ( ! queue . length ) return ;
hls ?. destroy ();
hls = new Hls ();
const url = queue . shift () ! ;
hls . loadSource ( url );
hls . attachMedia ( videoEl );
hls . on ( Hls . Events . MANIFEST_PARSED , () => videoEl . play ());
hls . on ( Hls . Events . ERROR , ( _ : any , err : any ) => {
if ( err . fatal ) tryNext ();
});
}
const res = await fetch ( endpoint , {
headers: { 'X-Session-Token' : token }
});
const reader = res . body ! . getReader ();
const decoder = new TextDecoder ();
let buffer = '' ;
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
buffer += decoder . decode ( value , { stream: true });
const lines = buffer . split ( ' \n ' );
buffer = lines . pop () ! ;
for ( const line of lines ) {
if ( ! line . startsWith ( 'data: ' )) continue ;
const event = JSON . parse ( line . slice ( 6 ));
if ( event . type === 'source' ) {
queue . push ( event . source . url );
if ( ! started ) { started = true ; tryNext (); }
}
if ( event . type === 'done' && ! started ) {
throw new Error ( 'No sources available' );
}
}
}
}
Step 4 — Add Subtitles
Subtitles arrive in the meta event, before any source. Attach them to your video element immediately.
if ( event . type === 'meta' ) {
event . subtitles . forEach (( sub , i ) => {
const track = document . createElement ( 'track' );
track . kind = 'subtitles' ;
track . label = sub . label ;
track . src = sub . file ;
track . default = i === 0 ;
videoEl . appendChild ( track );
});
}
Check Provider Health
Before building, verify which providers are currently live. The health endpoint accepts any valid API key or session token.
curl https://1c34-y.hf.space/api/health \
-H "Authorization: Bearer public_api_key"
{
"status" : "ok" ,
"timestamp" : "2025-01-01T00:00:00.000Z" ,
"tmdb" : true ,
"cache" : 0 ,
"probe_id" : "155" ,
"sources" : {
"provider-a" : { "ok" : true , "ms" : 1204 },
"provider-b" : { "ok" : true , "ms" : 980 },
"provider-c" : { "ok" : false , "ms" : 435 }
}
}
status: "degraded" means at least one provider is down — the rest still work and your fallback queue handles it automatically.
Debug a Single Provider
Use /api/test to isolate a specific provider without running the full fanout. This endpoint requires a standard/partner key or session token.
TOKEN = $( curl -s -X POST https://1c34-y.hf.space/api/auth \
-H "Authorization: Bearer YOUR_API_KEY" | jq -r .token )
curl "https://1c34-y.hf.space/api/test/550?source=<provider-key>" \
-H "X-Session-Token: $TOKEN "
Available provider keys are listed in the /api/health response under the sources field.
Full API Reference → See complete SSE event docs for every endpoint.