Partner API

Embed live streams on your site

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>
That is the whole integration. The rest of this page is detail: the rules, endpoints, code in your language, errors, what to do when nothing plays.

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.

  1. 1While rendering the page your viewer asked for, your backend callsPOST /api/v1/play_tokensand gets back a short-lived embed URL.
  2. 2You put that URL in an iframe. The player loads and plays.
Three rules.
  • 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 keytsg_… — 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 URLhttps://provider.theclandestineproject.online
Stream idsOr call the catalog endpoint below to list everything you are entitled to.
Registered domainsSend us every hostname you will embed from, including staging. They must be real dotted hostnames served over httpslocalhost cannot be registered.

Authentication

Every request carries your key as a bearer token.

Authorization: Bearer tsg_your_key_here

Endpoints

POST/api/v1/play_tokens

Mints 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" }
GET/api/v1/streams

Lists 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

  1. 1Both curl calls in the Quickstart return data with your production key.
  2. 2Your page mints a token on every load — reload twice and confirm the embed URL changes.
  3. 3The page is served over https from a hostname you have given us.
  4. 4The player shows video inside the iframe, not in a new tab.
  5. 5Your code handles a non-201 response by showing your own message rather than an empty frame.
Do not test by pasting the embed URL into your address bar.

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" } }

CodeHTTPMeaning
invalid_key401Key wrong, or revoked.
partner_suspended403Your account is suspended — contact us.
stream_not_found404No such stream, or not granted to you. Deliberately indistinguishable.
ambiguous_stream422The id exists in more than one app you hold; resend with an "app" field.
validation_error422Missing stream_id, or viewer_ref over 255 characters.
rate_limited429Over 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 causeHow to confirm
Token already used (cached or reused URL)Are you minting a fresh token per page load?
Token older than 2 minutesHow long between the API call and the page render?
Page not served from a registered domainCheck 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 iframeSee 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.