Quickstart
Before you can run anything, ask us for access.
Contact your account manager. We set up your partner account and give you an API key — a string starting withtsg_. Store it somewhere safe; if
it is lost we issue a new one and retire the old.
Tell us at the same time which domains you will embed from (including staging). We register them against your account, and the player refuses to load anywhere else.
You do not need stream ids in advance — step 1 below tells you which ones your key may play.
With the key in hand, run these. No web page needed yet.
GATE=https://provider.theclandestineproject.online
KEY=tsg_paste_your_key_here
# 1. Which streams may I play? Returns stream_id, app, title and status.
curl -s -H "Authorization: Bearer $KEY" $GATE/api/v1/streams
# 2. Mint an embed URL for ONE viewer, using a stream_id from step 1.
# Valid 2 minutes, works once.
curl -s -X POST $GATE/api/v1/play_tokens \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"stream_id":"PASTE_A_STREAM_ID_FROM_STEP_1"}'
Then put the embed_url you got back into an iframe:
<iframe src="EMBED_URL" width="960" height="540" frameborder="0"
allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
How it works
Integration is one server-side API call plus one iframe. You never handle video, players, or streaming protocols — those stay inside the embed.
- 1While rendering the page your viewer asked for, your backend calls
POST /api/v1/play_tokensand gets back a short-lived embed URL. - 2You put that URL in an iframe. The player loads and plays.
- A fresh token for every viewer, on every page load. Tokens are single-use and expire in 2 minutes. Never cache one, reuse one across viewers, or put the same URL on two pages.
- Your API key never leaves your server. Calling the token endpoint from browser JavaScript exposes it to every visitor.
- Embed only from registered domains. A correct URL on an unregistered domain renders nothing — the browser blocks it.
What you receive
| API key | tsg_… — a server-side secret. We issue it and show it to you exactly once at creation; tell us immediately if it leaks and we will revoke it. |
| Gate URL | https://provider.theclandestineproject.online |
| Stream ids | Or call the catalog endpoint below to list everything you are entitled to. |
| Registered domains | Send us every hostname you will embed from, including staging. They must be
real dotted hostnames served over https —localhost cannot be registered. |
Authentication
Every request carries your key as a bearer token.
Authorization: Bearer tsg_your_key_hereEndpoints
/api/v1/play_tokensMints one single-use embed URL. Call it once per viewer, per page load.
Request
{ "stream_id": "match42", "viewer_ref": "your-internal-user-id" }viewer_ref is optional. It is recorded in our audit log so we can trace a playback back to
one of your users; it never reaches the browser.
Response · 201
{ "embed_url": "https://provider.theclandestineproject.online/embed/cdRB8TQ…", "expires_at": "2026-08-09T18:34:43+08:00" }/api/v1/streamsLists the streams your key is entitled to play.
Response · 200
[{ "stream_id": "match42", "app": "TestStream", "title": "Match 42", "status": "live" }]Code
<?php
// Runs on YOUR server. The API key must never reach the browser.
$ch = curl_init('https://provider.theclandestineproject.online/api/v1/play_tokens');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('STREAM_GATE_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'stream_id' => 'match42',
'viewer_ref' => $currentUserId, // optional
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 201) {
// Log $body, show your own "stream unavailable" state.
return;
}
$embedUrl = json_decode($body, true)['embed_url'];
?>
<iframe src="<?= htmlspecialchars($embedUrl, ENT_QUOTES) ?>"
width="960" height="540" frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen></iframe>
The iframe
<iframe src="EMBED_URL" width="960" height="540" frameborder="0"
allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
The player starts muted with click-to-unmute, because browsers block autoplay with sound. It uses WebRTC for sub-second latency and falls back to HLS on restrictive networks. Video flows directly from our media server to the viewer — it does not pass through your servers.
Before you go live
- 1Both curl calls in the Quickstart return data with your production key.
- 2Your page mints a token on every load — reload twice and confirm the embed URL changes.
- 3The page is served over https from a hostname you have given us.
- 4The player shows video inside the iframe, not in a new tab.
- 5Your code handles a non-201 response by showing your own message rather than an empty frame.
It will always show "Stream unavailable". Opening it as a top-level page is refused on purpose — the domain lock has no framing context to check there, so a leaked URL would otherwise play anywhere. It must be loaded inside an iframe, on a page served over https from a registered domain.
Errors
Failures return{ "error": { "code", "message" } }
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 401 | Key wrong, or revoked. |
partner_suspended | 403 | Your account is suspended — contact us. |
stream_not_found | 404 | No such stream, or not granted to you. Deliberately indistinguishable. |
ambiguous_stream | 422 | The id exists in more than one app you hold; resend with an "app" field. |
validation_error | 422 | Missing stream_id, or viewer_ref over 255 characters. |
rate_limited | 429 | Over 60 token requests per minute. Retry shortly. |
When the iframe shows "Stream unavailable"
Every rejection renders the same page on purpose — an attacker must not learn which check failed. The cause is one of:
| Likely cause | How to confirm |
|---|---|
| Token already used (cached or reused URL) | Are you minting a fresh token per page load? |
| Token older than 2 minutes | How long between the API call and the page render? |
| Page not served from a registered domain | Check the exact hostname, including subdomain — a www. prefix counts as different. |
| Page served over http:// | The domain lock is https-only. |
| Opened directly instead of in an iframe | See the warning above. |
If none of those fit, send us the approximate time of the attempt. Our token log records every issue and render with its source, and we can tell you exactly which check refused it.