Football stream embed API for live match players
One call returns a ready-made embedUrl for every source of a match. Drop it in an iframe, add a source switcher, and handle the three rules that decide whether it plays at all: referrer, iframe, no sandbox.
Updated 2026-10-26 8 min read
Embedding a match is one call and one iframe. The API hands you a finished URL per source — you never touch a stream address, build a token, or pick a player.
What takes the time is the three rules that decide whether it plays at all. Those are at the bottom, and they are the part worth reading twice.
1. Check before you render
The fixture list you already fetched tells you whether a watch tab is worth showing. Reading it costs nothing:
// from type=matches or type=scores
if (!match.has_stream) {
// no watch tab at all
} 2. One call gives you everything
GET https://api.sportsrc.org/v3/?type=detail&id=MATCH_ID
Inside the response, data.sources is the list you want:
"sources": [
{
"id": "stream-1",
"streamNo": 1,
"embedUrl": "https://embed.example.com/?id=MATCH_ID&source=lola",
"source": "lola",
"hd": true,
"language": "Unknown"
}
] embedUrl is complete. It already carries the signed match id
and the source name, so there is nothing to assemble.
3. Drop it in an iframe
<iframe
src={sources[0].embedUrl}
allow="autoplay; fullscreen; encrypted-media; picture-in-picture"
allowfullscreen>
</iframe> 4. Offer a source switcher
The highest-value thing you can build, and it is four lines. Sources come from third parties and fail independently; a viewer whose source dies at 60 minutes either clicks one button or closes your site.
const { sources } = (await api(`?type=detail&id=${id}`)).data;
sources.forEach(s => {
const btn = document.createElement('button');
btn.textContent = s.source.toUpperCase();
btn.onclick = () => { iframe.src = s.embedUrl; };
bar.appendChild(btn);
}); Keep the bar visible during playback, not hidden in a menu.
5. The three rules that decide whether it plays
This is where embeds actually fail. All three are enforced, and breaking any one of them is refused rather than degraded.
Your domain must be whitelisted, and must send a Referer
The player checks the Referer header against a whitelist. Two
consequences:
- Your domain has to be on the list. Subdomains of a listed domain are included.
-
referrerpolicy="no-referrer"will break the embed. No header means no domain to check. If your site sets a referrer policy, useoriginorstrict-origin-when-cross-origin.
This is the most common failure and the hardest to diagnose from the outside, because a page that works on one site fails on another for a reason nothing in the player's output explains.
It must be inside an iframe
Opening the URL directly in a tab is refused. The player checks
Sec-Fetch-Dest, and anything that is not
iframe — including a request with no such header, which
is what a script sends — is rejected.
Do not sandbox the iframe
<!-- rejected -->
<iframe sandbox="allow-scripts allow-same-origin" src="..."></iframe>
A sandboxed iframe is refused outright. The reason is specific: a sandbox
without allow-popups silently kills the player's monetisation
while the stream still plays, so the embedder gets the video and the
provider gets nothing. If your CSP forces a sandbox on third-party frames,
the embed will not work and no attribute combination changes that.
6. The three states that actually occur
Not started, no source yet
Sources appear close to kick-off — often inside the final fifteen
minutes, sometimes after it. A fixture reporting
has_stream: false six hours out may well report true at
kick-off.
So re-read it on the poll you already run for scores, and let the watch tab appear while the page is open. The viewer who arrived early is the one most likely to still be there.
In play, a source drops
Nothing notifies you. The switcher is the recovery path, which is why it stays on screen.
Finished
A source can linger after full time. Gate on status too:
const watchable =
match.has_stream &&
(match.status === 'inprogress' ||
(match.status === 'notstarted' && minutesToKickoff(match) < 30)); What the embed is not
It is a player, not a video file. You cannot restyle its inside, and the stream carries whatever advertising its upstream source carries — on every plan, because the ads belong to the source rather than to SportSRC.
If you need raw m3u8 links in your own player with no injected
advertising, that is a restream service rather than an aggregator.
What that costs and why.
Summary
- Check
has_streamfirst. - One
detailcall gives a finishedembedUrlper source. - Whitelisted domain, a real
Referer, inside an iframe, nosandbox. - Always offer a switcher — sources fail independently.
- Re-read availability as you poll; it changes around kick-off.
Frequently asked
How do I embed a live football match on my website?
Call the detail endpoint with the match id. Each entry in its sources array carries a finished embedUrl - put that straight into an iframe src. You never handle a stream URL or build one yourself.
Why does the player show Access Denied?
Three rules decide this. The page must send a Referer from a whitelisted domain, the player must be loaded inside an iframe rather than opened directly, and the iframe must not be sandboxed. Breaking any one of them is refused.
Can I use sandbox on the iframe?
No. A sandboxed iframe is rejected outright, because sandboxing silently disables the player’s monetisation while still consuming the stream. If your CSP forces a sandbox, the embed will not play.
Why does a match have several sources?
Different providers publish the same match. Offering all of them means a viewer whose source dies can switch in one click instead of losing the match.
What if a match has no stream?
has_stream is false and the sources array is empty. Check the flag before rendering a watch tab at all.