Test your stream and integrate with our API
Get your API key from the dashboard after adding a "Self Hosted" destination
Get your Permanent Key from Dashboard -> Developer Tools -> API Keys ->
Permanent Keys
or use your temporary Session Token from Dashboard -> Developer Tools -> API Keys -> Session Tokens.
Goal: Embed your live stream on your website and track cost.
pk_...)
GET /api/public/stream/status
hlsUrl in your video player
That's it! See the full example below.
Poll this to check if your stream is live.
Stable URL for Shosho or your custom player. Playable only when live.
| Key Type | Prefix | Used For |
|---|---|---|
| Public API Key | pk_... |
Read-only stream status, playback, cost tracking |
| Permanent API Key | sk_... |
Webhooks, developer tools, management API |
๐ก Most developers only need a Public API Key to get started.
GET /api/public/stream/status
Returns the current live/offline status of your stream, HLS playback URL, and bandwidth usage information.
Provide your API key using one of these methods (in order of preference):
X-API-Key: YOUR_API_KEY
Authorization: Bearer YOUR_API_KEY
?apiKey=YOUR_API_KEY or ?key=YOUR_API_KEY
60 requests per minute per API key. Exceeding this limit
will return a 429 Too Many Requests error.
60 requests per minute shared across all viewers. Designed for testing and small previews (1-2 concurrent viewers).
Unlimited concurrent viewers. Scaling is managed by our infrastructure and billed only for bandwidth (10 GB/hr included free).
{
"isLive": true,
"hlsReady": true,
"hlsUrl": "https://simulcast.me/hls/abc123/index.m3u8?apiKey=...",
"streamKeyLabel": "My Gaming Stream",
"startedAt": 1234567890,
"playbackToken": "abc123...",
"dvr": {
"enabled": true,
"windowSeconds": 1800,
"maxWindowSeconds": 3600,
"availableFrom": 1234567890
},
"bandwidth": {
"usedGB": "2.45",
"limitGB": 10,
"remainingGB": "7.55",
"overageGB": "0.00",
"overageSats": 0,
"hourStart": 1234560000
}
}
{
"isLive": false,
"hlsReady": false,
"hlsUrl": null,
"streamKeyLabel": "My Gaming Stream",
"startedAt": null,
"playbackToken": null,
"dvr": {
"enabled": false,
"windowSeconds": 0,
"maxWindowSeconds": 3600,
"availableFrom": null
},
"bandwidth": {
"usedGB": "0.00",
"limitGB": 10,
"remainingGB": "10.00",
"overageGB": "0.00",
"overageSats": 0,
"hourStart": 1234560000
}
}
isLive (boolean) - Whether
the stream is currently livehlsReady (boolean) - Whether
the HLS playlist is fully initialized and ready for playbackhlsUrl (string|null) -
Permanent HLS playback URL (includes API key in query string)label (string) -
Stream label (alias for streamKeyLabel)dvr (object) - Rolling buffer
(DVR-style rewind) information.
DVR allows viewers to rewind a live stream up to a configurable
window without creating permanent recordings.
enabled - Whether rolling buffer is available (only when
live)windowSeconds - Current buffer window in secondsmaxWindowSeconds - Maximum buffer window (3600 = 1 hour)
availableFrom - Timestamp when buffer startedstreamKeyLabel (string) -
Stream label
โ ๏ธ Deprecated. Use label instead. Will be removed in
v1.1.
startedAt (number|null) -
Unix timestamp when stream startedplaybackToken (string|null) -
Secure token for HLS preview (not needed for API)bandwidth (object) -
Bandwidth usage information for current hourusedGB - GB used this hourlimitGB - Free tier limit (10
GB/hour)remainingGB - GB remaining in free tieroverageGB - GB over limit (charged at 5 sats/GB)overageSats - Sats charged for overage this hourGET /api/public/config
Returns the current pricing and bandwidth configuration for the service. Use this to ensure your UI matches the latest backend rates.
{
"pricing": {
"BANDWIDTH_LIMIT_GB_PER_HOUR": 10,
"BANDWIDTH_OVERAGE_SATS_PER_GB": 5,
"COST_PER_PLATFORM": 2,
"FREE_PLATFORMS": 1
},
"version": "0.5.0",
"timestamp": 1234567890
}
GET /api/public/streams/active/cost
Returns the total cost spent on currently active streams. Accessible via API Key.
{
"hasActiveStream": true,
"totalCost": 15,
"streams": [
{
"sessionId": "ABC123",
"streamKeyId": 3,
"label": "My Stream",
"streamKeyLabel": "My Stream",
"startedAt": 1769644800000,
"durationMinutes": 5,
"durationSeconds": 30,
"totalCharged": 10,
"ratePerMinute": 2
}
]
}
{
"hasActiveStream": false,
"totalCost": 0,
"streams": []
}
{
"error": "Invalid API key or destination disabled"
}
{
"error": "Rate limit exceeded. Maximum 60 requests per minute.",
"retryAfter": 30
}
{
"error": "API key required. Provide via X-API-Key header, Authorization: Bearer header, or ?apiKey= query parameter."
}
These endpoints require authentication with your user session token (cookie-based auth from the dashboard). They provide access to stream management, analytics, and account data.
Requires a session token (automatically handled when logged into the dashboard).
For programmatic access, you can use the Permanent API Key
(sk_...) with Bearer token authentication.
GET /api/bandwidth/usage
Returns bandwidth usage statistics for your account, with optional time filtering.
filter (string, optional) -
Time filter: hour, day, week, month,
year, all. Default: hour
startDate (number,
optional)
- Unix timestamp (ms) for custom date range startendDate (number, optional)
-
Unix timestamp (ms) for custom date range end{
"totalGB": 14.65,
"overageGB": 2.30,
"overageSats": 12
}
totalGB - Total bandwidth used in the selected time period
overageGB - Bandwidth over the free tier limit (10 GB/hour)
overageSats - Cost in sats for overage (5 sats/GB)hour filter shows current hour
usage, while all shows total historical usage including deleted destinations.
GET /api/dashboard/stats
Returns aggregated statistics for total streaming time and cost.
filter (string, optional) -
Time filter: day, week, month, year,
all. Default: day
startDate (number,
optional)
- Unix timestamp (ms) for custom date rangeendDate (number, optional)
-
Unix timestamp (ms) for custom date range{
"totalMinutes": 368.6,
"totalSatsSpent": 105
}
totalMinutes - Total streaming time in minutestotalSatsSpent - Total cost in satoshis (2 sats/min base rate)
GET /api/stream-history
Returns detailed history of past streaming sessions with filtering options.
startDate (number,
optional)
- Filter by start date (Unix timestamp ms)endDate (number, optional)
-
Filter by end date (Unix timestamp ms)minCost (number, optional)
-
Minimum cost filter (sats)maxCost (number, optional)
-
Maximum cost filter (sats)platform (string, optional)
-
Filter by platform namestreamKeyId (number,
optional) - Filter by stream key ID[
{
"id": 123,
"streamKeyId": 5,
"streamKeyLabel": "My Gaming Stream",
"sessionId": "abc123",
"platform": "Twitch",
"startedAt": 1770012000000,
"endedAt": 1770015600000,
"durationMinutes": 60.0,
"totalCostSats": 120
}
]
List all your stream keys.
[
{
"id": 12,
"streamKey": "def19b4b...",
"label": "My Event Stream",
"status": "idle",
"createdAt": 1769644800000
}
]
GET /api/streams/{id}Get details for a specific stream.
{
"id": 12,
"streamKey": "def19b4b...",
"label": "My Event Stream",
"status": "active",
"limits": { "maxSats": 1000 },
"startedAt": 1769644800000,
"createdAt": 1769640000000,
"destinations": [
{
"id": 45,
"type": "rtmp",
"platform": "Twitch",
"enabled": true,
"url": "rtmp://..."
}
]
}
GET /api/streams/{id}/destinationsList destinations for a specific stream.
[
{
"id": 45,
"type": "rtmp",
"platform": "Twitch",
"enabled": true,
"url": "rtmp://..."
}
]
POST /api/streamsCreates a new programmable stream key.
{
"label": "My Event Stream"
}
{
"id": 12,
"streamKey": "def19b4b...",
"label": "My Event Stream",
"status": "idle",
"limits": {}
}
POST /api/streams/{id}/destinationsAdds a publishing destination to the stream.
{
"type": "rtmp",
"config": {
"url": "rtmp://live.twitch.tv/app/KEY"
}
}
{
"success": true,
"id": 45
}
POST /api/streams/{id}/limitsEnforce hard limits on duration or budget. Stream stops automatically when reached.
{
"maxSats": 1000,
"maxDurationMinutes": 60
}
{
"success": true
}
POST /api/streams/{id}/stopForcefully terminates the active streaming session.
{
"success": true,
"message": "Stop signal sent"
}
GET /api/wallet/balanceRetrieve your current wallet balance.
{
"balance": 1050,
"cached": false
}
Create a new stream, optionally with a list of initial destinations (Unified Creation).
{
"label": "My Awesome Stream",
"destinations": [
{
"type": "rtmp",
"config": {
"url": "rtmp://live.twitch.tv/app/live_...",
"key": ""
}
},
{ "type": "self-hosted" }
]
}
Supported Types: rtmp, twitch, self-hosted,
cloud-archive.
{
"id": 12,
"streamKey": "8f7...a1",
"label": "My Awesome Stream",
"destinationsCount": 2
}
Manage stream destinations (RTMP, Twitch, Self-Hosted, Cloud Archive).
Delete a destination by its ID. Quick and simple when you only have the destination ID.
id: Destination ID{"success": true}
Delete a destination with explicit stream ownership validation. Safer for production use.
id: Stream Key IDdestId: Destination ID{"success": true}
Manage local stream archives and recovery.
Recover a failed S3 upload by paying a network fee. Requires NWC connection.
{
"fileName": "archive_StreamKey_Timestamp.mp4"
}
{
"success": true,
"message": "Payment accepted. Archive recovery started."
}
Configure webhooks to receive real-time events about your streams.
๐ Authentication: Webhook management requires a Permanent API Key
(sk_...).
Public API keys (pk_...) cannot manage webhooks.
List all configured webhooks.
Register a new webhook endpoint.
{
"url": "https://your-api.com/callback",
"events": ["stream.started", "stream.ended", "recording.ready"],
"secret": "your_signing_secret",
"stream_key_id": 3 // Optional: null or omit for all streams
}
url (required): HTTPS endpoint to receive webhook eventsevents (required): Array of event names to subscribe tosecret (optional): Signing secret for HMAC signature verificationstream_key_id (optional): Specific stream key ID, or null/omit for all streamsstream.started: Triggered when a stream begins receiving datastream.ended: Triggered when a stream stops (includes duration/cost stats)recording.ready: Triggered when cloud archive upload completestest.ping: Triggered when testing a webhook from the dashboardRemove a webhook configuration.
curl -H "X-API-Key: YOUR_API_KEY" \
https://simulcast.me/api/public/stream/status
curl "https://simulcast.me/api/public/stream/status?apiKey=YOUR_API_KEY"
curl -H "X-API-Key: YOUR_API_KEY" \
https://simulcast.me/api/public/streams/active/cost
const API_KEY = 'YOUR_API_KEY';
const API_URL = 'https://simulcast.me/api/public/stream/status';
async function checkStream() {
try {
const response = await fetch(API_URL, {
headers: { 'X-API-Key': API_KEY }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.isLive) {
console.log('Stream is LIVE:', data.hlsUrl);
// Use data.hlsUrl in your video player
} else {
console.log('Stream is OFFLINE');
}
} catch (error) {
console.error('Error:', error);
}
}
// Check every 5 seconds
checkStream();
setInterval(checkStream, 5000);
async function getActiveCost() {
const response = await fetch('https://simulcast.me/api/public/streams/active/cost', {
headers: { 'X-API-Key': API_KEY }
});
const data = await response.json();
if (data.hasActiveStream) {
console.log(`Total cost: ${data.totalCost} sats`);
data.streams.forEach(stream => {
console.log(`${stream.label}: ${stream.durationMinutes}m ${stream.durationSeconds}s`);
});
}
}
import requests
API_KEY = "YOUR_API_KEY"
API_URL = "https://simulcast.me/api/public/stream/status"
# Using header (recommended)
response = requests.get(
API_URL,
headers={"X-API-Key": API_KEY}
)
if response.status_code == 200:
data = response.json()
if data["isLive"]:
print(f"Stream is LIVE: {data['hlsUrl']}")
print(f"Bandwidth: {data['bandwidth']['usedGB']} GB used")
else:
print("Stream is OFFLINE")
else:
print(f"Error: {response.status_code} - {response.text}")
response = requests.get(
"https://simulcast.me/api/public/streams/active/cost",
headers={"X-API-Key": API_KEY}
)
data = response.json()
if data["hasActiveStream"]:
print(f"Total cost: {data['totalCost']} sats")
for stream in data["streams"]:
print(f"{stream['label']}: {stream['durationMinutes']}m {stream['durationSeconds']}s")
const API_KEY = 'YOUR_API_KEY';
const API_URL = 'https://simulcast.me/api/public/stream/status';
// Using fetch
async function checkStream() {
try {
const response = await fetch(API_URL, {
headers: { 'X-API-Key': API_KEY }
});
const data = await response.json();
if (data.isLive) {
console.log('Stream is LIVE:', data.hlsUrl);
} else {
console.log('Stream is OFFLINE');
}
} catch (error) {
console.error('Error:', error);
}
}
// Or using axios
const axios = require('axios');
axios.get(API_URL, {
headers: { 'X-API-Key': API_KEY }
})
.then(response => {
const data = response.data;
if (data.isLive) {
console.log('Stream is LIVE:', data.hlsUrl);
}
})
.catch(error => console.error('Error:', error));
async function getActiveCost() {
const response = await fetch('https://simulcast.me/api/public/streams/active/cost', {
headers: { 'X-API-Key': API_KEY }
});
const data = await response.json();
if (data.hasActiveStream) {
console.log(`Total: ${data.totalCost} sats`);
}
}
<!DOCTYPE html>
<html>
<head>
<title>My Stream</title>
<script src="https://vjs.zencdn.net/8.6.1/video.min.js"></script>
<link href="https://vjs.zencdn.net/8.6.1/video-js.css" rel="stylesheet">
</head>
<body>
<div id="status">Loading...</div>
<div id="cost"></div>
<video id="video" class="video-js" controls style="display:none;"></video>
<script>
const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://simulcast.me/api/public';
async function updateDashboard() {
try {
// Check stream status
const streamRes = await fetch(`${BASE_URL}/stream/status`, {
headers: { 'X-API-Key': API_KEY }
});
const streamData = await streamRes.json();
const statusDiv = document.getElementById('status');
const video = document.getElementById('video');
if (streamData.isLive) {
statusDiv.textContent = `๐ด LIVE: ${streamData.label}`;
videojs('video').src({
type: 'application/x-mpegURL',
src: streamData.hlsUrl
});
video.style.display = 'block';
} else {
statusDiv.textContent = 'โซ OFFLINE';
video.style.display = 'none';
}
// Get active cost
const costRes = await fetch(`${BASE_URL}/streams/active/cost`, {
headers: { 'X-API-Key': API_KEY }
});
const costData = await costRes.json();
document.getElementById('cost').textContent =
costData.hasActiveStream
? `๐ฐ Current cost: ${costData.totalCost} sats`
: '๐ฐ No active streams';
} catch (error) {
console.error('Error updating dashboard:', error);
}
}
updateDashboard();
setInterval(updateDashboard, 5000);
</script>
</body>
</html>
curl -X POST https://simulcast.me/api/streams \
-H "Authorization: Bearer YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label": "My Event Stream"}'
curl -X POST https://simulcast.me/api/streams/12/destinations \
-H "Authorization: Bearer YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "rtmp",
"config": {
"url": "rtmp://live.twitch.tv/app/YOUR_STREAM_KEY"
}
}'
curl -X POST https://simulcast.me/api/streams/12/limits \
-H "Authorization: Bearer YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"maxSats": 1000,
"maxDurationMinutes": 60
}'
curl -X POST https://simulcast.me/api/streams/12/stop \
-H "Authorization: Bearer YOUR_SESSION_TOKEN"
const SESSION_TOKEN = 'YOUR_SESSION_TOKEN';
const BASE_URL = 'https://simulcast.me/api';
async function createStream(label) {
const response = await fetch(`${BASE_URL}/streams`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${SESSION_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ label })
});
const data = await response.json();
console.log('Created stream:', data.id, data.streamKey);
return data;
}
// Usage
createStream('My Event Stream');
async function addDestination(streamId, type, url) {
const response = await fetch(`${BASE_URL}/streams/${streamId}/destinations`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${SESSION_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: type,
config: { url: url }
})
});
const data = await response.json();
console.log('Added destination:', data.id);
return data;
}
// Usage
addDestination(12, 'rtmp', 'rtmp://live.twitch.tv/app/YOUR_KEY');
async function setLimits(streamId, maxSats, maxDurationMinutes) {
const response = await fetch(`${BASE_URL}/streams/${streamId}/limits`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${SESSION_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
maxSats,
maxDurationMinutes
})
});
const data = await response.json();
console.log('Limits set:', data);
return data;
}
// Usage
setLimits(12, 1000, 60);
async function stopStream(streamId) {
const response = await fetch(`${BASE_URL}/streams/${streamId}/stop`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${SESSION_TOKEN}`
}
});
const data = await response.json();
console.log('Stream stopped:', data.message);
return data;
}
// Usage
stopStream(12);
import requests
SESSION_TOKEN = "YOUR_SESSION_TOKEN"
BASE_URL = "https://simulcast.me/api"
def create_stream(label):
response = requests.post(
f"{BASE_URL}/streams",
headers={
"Authorization": f"Bearer {SESSION_TOKEN}",
"Content-Type": "application/json"
},
json={"label": label}
)
data = response.json()
print(f"Created stream: {data['id']}, Key: {data['streamKey']}")
return data
# Usage
stream = create_stream("My Event Stream")
def add_destination(stream_id, dest_type, url):
response = requests.post(
f"{BASE_URL}/streams/{stream_id}/destinations",
headers={
"Authorization": f"Bearer {SESSION_TOKEN}",
"Content-Type": "application/json"
},
json={
"type": dest_type,
"config": {"url": url}
}
)
data = response.json()
print(f"Added destination: {data['id']}")
return data
# Usage
add_destination(12, "rtmp", "rtmp://live.twitch.tv/app/YOUR_KEY")
def set_limits(stream_id, max_sats, max_duration_minutes):
response = requests.post(
f"{BASE_URL}/streams/{stream_id}/limits",
headers={
"Authorization": f"Bearer {SESSION_TOKEN}",
"Content-Type": "application/json"
},
json={
"maxSats": max_sats,
"maxDurationMinutes": max_duration_minutes
}
)
data = response.json()
print(f"Limits set: {data}")
return data
# Usage
set_limits(12, 1000, 60)
def stop_stream(stream_id):
response = requests.post(
f"{BASE_URL}/streams/{stream_id}/stop",
headers={
"Authorization": f"Bearer {SESSION_TOKEN}"
}
)
data = response.json()
print(f"Stream stopped: {data['message']}")
return data
# Usage
stop_stream(12)
const axios = require('axios');
const SESSION_TOKEN = 'YOUR_SESSION_TOKEN';
const BASE_URL = 'https://simulcast.me/api';
const api = axios.create({
baseURL: BASE_URL,
headers: {
'Authorization': `Bearer ${SESSION_TOKEN}`,
'Content-Type': 'application/json'
}
});
// Create stream
async function createStream(label) {
const { data } = await api.post('/streams', { label });
console.log('Created stream:', data.id);
return data;
}
// Add destination
async function addDestination(streamId, type, url) {
const { data } = await api.post(`/streams/${streamId}/destinations`, {
type,
config: { url }
});
console.log('Added destination:', data.id);
return data;
}
// Set limits
async function setLimits(streamId, maxSats, maxDurationMinutes) {
const { data } = await api.post(`/streams/${streamId}/limits`, {
maxSats,
maxDurationMinutes
});
console.log('Limits set');
return data;
}
// Stop stream
async function stopStream(streamId) {
const { data } = await api.post(`/streams/${streamId}/stop`);
console.log('Stream stopped:', data.message);
return data;
}
// Usage example
async function setupEventStream() {
try {
// Create a new stream
const stream = await createStream('Conference Stream');
// Add Twitch destination
await addDestination(
stream.id,
'rtmp',
'rtmp://live.twitch.tv/app/YOUR_KEY'
);
// Set budget and time limits
await setLimits(stream.id, 1000, 120);
console.log('Stream ready! Use this key:', stream.streamKey);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
setupEventStream();
You can play your HLS stream directly in VLC Media Player using the HLS URL from the API response.
Ctrl+N /
Cmd+N)
data.hlsUrl)vlc "https://simulcast.me/hls/YOUR_TOKEN/index.m3u8?apiKey=YOUR_API_KEY"
A WordPress plugin to easily embed your self-hosted Simulcast.me livestream using the API and HLS player.
To accept tips, simply install and activate WooCommerce. The plugin will automatically create a hidden "Stream Tip" product for you. Users can click "Support the Stream" on the player, select an amount, and checkout via your existing WooCommerce payment gateways.
Bitcoin Payment Options: To accept Bitcoin tips via Lightning Network, you can install one of these payment plugins:
/wp-content/plugins/ directoryEmbed the player on any page or post using the shortcode:
[simulcast_player]
If you use 12 GB in one hour, you'll be charged:
โข 2 sats/min for streaming (if not first destination)
โข 10 sats/hour for overage (2 GB @ 5 sats/GB)
Total: 12 sats/min (streaming) + bandwidth