LiveRelay Documentation
A Rust-powered WebRTC SFU as-a-service. Stream live video in 5 minutes.
Quick Start
Go from zero to live streaming in five steps. All you need is a running LiveRelay server and a browser.
When you start the LiveRelay server, a bootstrap API key is logged to stdout:
INFO LiveRelay SFU listening on http://0.0.0.0:8080 INFO Bootstrap API key: lr_aebfc064dd0c8fa2...
Or create one via the Dashboard.
curl -X POST https://your-server.com/v1/rooms \ -H "Authorization: Bearer lr_your_api_key" \ -H "Content-Type: application/json" \ -d '{"room_type": "broadcast"}'
Response:
{
"id": "8271274b-fc62-4b73-98c5-40e28b7a1b81",
"type": "broadcast",
"tokens": {
"publish": "eyJ...",
"subscribe": "eyJ..."
}
}
Include via a script tag:
<script src="https://your-server.com/static/liverelay.js"></script>
Or via npm:
npm install @liverelay/sdk
const lr = new LiveRelay('https://your-server.com'); const session = await lr.publish({ token: 'eyJ...publish_token', video: true, audio: true }); console.log(session.state); // 'connected'
const session = await lr.subscribe({ token: 'eyJ...subscribe_token', element: document.getElementById('video') });
Authentication
LiveRelay uses a two-level authentication system that separates backend management from user-facing connections.
| Level | Who | How | Used for |
|---|---|---|---|
| API Key | Your backend | Authorization: Bearer lr_... |
REST API (create rooms, tokens) |
| JWT Token | Your users | Authorization: Bearer eyJ... |
SFU endpoints (publish, subscribe, call) |
Authentication Flow
API Key format
lr_ + 32 hex characters, e.g. lr_aebfc064dd0c8fa2cb0cbc01998e3f9b
JWT Token claims
{
"sub": "peer-uuid",
"room_id": "room-uuid",
"role": "publish|subscribe|call",
"key_id": "lr_aebfc",
"exp": 1770498859,
"iat": 1770412459
}
Tokens expire after 24 hours by default.
REST API Reference
All REST endpoints require an API key unless noted otherwise. Send it via the Authorization: Bearer lr_... header.
Creates a new broadcast or call room and returns join tokens.
curl -X POST https://your-server.com/v1/rooms \ -H "Authorization: Bearer lr_your_api_key" \ -H "Content-Type: application/json" \ -d '{"room_type": "broadcast"}'
Request body
| Field | Type | Description |
|---|---|---|
room_type | string | "broadcast" (1 publisher, N subscribers) or "call" (2 peers, bidirectional) |
Response — broadcast
{
"id": "uuid",
"type": "broadcast",
"tokens": {
"publish": "jwt...",
"subscribe": "jwt..."
}
}
Response — call
{
"id": "uuid",
"type": "call",
"tokens": {
"caller": "jwt...",
"callee": "jwt..."
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | auth_header_missing | Missing Authorization header |
| 401 | api_key_invalid | Invalid API key |
curl https://your-server.com/v1/rooms \
-H "Authorization: Bearer lr_your_api_key"
Response
[
{
"room_id": "uuid",
"room_type": "broadcast",
"publisher_count": 1,
"subscriber_count": 42,
"created_at_secs": 120
}
]
Returns the same object as a single list item.
curl https://your-server.com/v1/rooms/8271274b-fc62-4b73-98c5-40e28b7a1b81 \
-H "Authorization: Bearer lr_your_api_key"
Errors
| Status | Code |
|---|---|
| 404 | room_not_found |
curl -X DELETE https://your-server.com/v1/rooms/8271274b-fc62-4b73-98c5-40e28b7a1b81 \
-H "Authorization: Bearer lr_your_api_key"
Returns 204 No Content on success.
Errors
| Status | Code |
|---|---|
| 404 | room_not_found |
Generate an additional JWT token for an existing room.
curl -X POST https://your-server.com/v1/rooms/uuid/token \ -H "Authorization: Bearer lr_your_api_key" \ -H "Content-Type: application/json" \ -d '{"role": "subscribe"}'
Request body
| Field | Type | Description |
|---|---|---|
role | string | "publish", "subscribe", or "call" |
Response
{
"token": "jwt..."
}
Errors
| Status | Code |
|---|---|
| 404 | room_not_found |
| 400 | invalid_role |
Requires an existing API key. Creates a new API key for programmatic access.
curl -X POST https://your-server.com/v1/keys \ -H "Authorization: Bearer lr_your_api_key" \ -H "Content-Type: application/json" \ -d '{"name": "my-app"}'
Response
{
"key": "lr_...",
"name": "my-app"
}
curl https://your-server.com/health
Response
{
"status": "ok",
"version": "0.2.0",
"rooms_active": 5,
"subscribers_active": 12
}
SFU Endpoints
These endpoints use JWT token authentication (not API keys). The token's role claim determines access.
Requires a JWT with role "publish" or "call".
curl -X POST https://your-server.com/sfu/publish \ -H "Authorization: Bearer eyJ...jwt_token" \ -H "Content-Type: application/json" \ -d '{"sdp": "v=0...", "type": "offer"}'
Request body
| Field | Type | Description |
|---|---|---|
sdp | string | WebRTC SDP offer |
type | string | Always "offer" |
Response
{
"sdp": "v=0...",
"type": "answer"
}
Requires a JWT with role "subscribe" or "call".
curl -X POST https://your-server.com/sfu/subscribe \ -H "Authorization: Bearer eyJ...jwt_token" \ -H "Content-Type: application/json" \ -d '{"sdp": "v=0...", "type": "offer"}'
Response
{
"sdp": "v=0...",
"type": "answer"
}
Requires a JWT with role "call".
curl -X POST https://your-server.com/sfu/call \ -H "Authorization: Bearer eyJ...jwt_token" \ -H "Content-Type: application/json" \ -d '{"sdp": "v=0...", "type": "offer"}'
Response
{
"sdp": "v=0...",
"type": "answer"
}
SDK Reference
Installation
<script src="https://your-server.com/static/liverelay.js"></script> <!-- Available as window.LiveRelay -->
Constructor
new LiveRelay(serverUrl) new LiveRelay({ server: serverUrl }) LiveRelay.VERSION // "0.2.0"
lr.publish(options) → Promise<PublishSession>
const session = await lr.publish({ token: string, // Required: JWT publish token stream: MediaStream, // Option A: provide your own stream video: boolean, // Option B: auto-capture (default: false) audio: boolean, // Option B: auto-capture (default: false) onStateChange: function // Optional: callback(state) });
lr.subscribe(options) → Promise<SubscribeSession>
const session = await lr.subscribe({ token: string, // Required: JWT subscribe token element: HTMLVideoElement, // Required: video element to render to onStateChange: function // Optional: callback(state) });
lr.call(options) → Promise<CallSession>
const session = await lr.call({ token: string, // Required: JWT call token stream: MediaStream, // Option A: provide your own stream video: boolean, // Option B: auto-capture audio: boolean, // Option B: auto-capture element: HTMLVideoElement, // Required: for remote video localElement: HTMLVideoElement, // Optional: local preview onStateChange: function // Optional: callback(state) });
Session properties & methods
Common to all session types:
session.state // 'connecting' | 'connected' | 'disconnected' | 'failed' session.close() // Close the session
Mute controls (PublishSession & CallSession)
session.muteAudio() session.unmuteAudio() session.muteVideo() session.unmuteVideo() session.isAudioMuted // boolean session.isVideoMuted // boolean
LiveRelayError
try { await lr.publish({ token, video: true }); } catch (e) { if (e instanceof LiveRelayError) { console.log(e.code); // 'AUTH_ERROR', 'NETWORK_ERROR', etc. console.log(e.message); // Human-readable message console.log(e.httpStatus); // HTTP status code (if from server) } }
SDK Error codes
| Code | Description |
|---|---|
AUTH_ERROR | Token is invalid, expired, or has wrong role |
NETWORK_ERROR | Failed to connect to the server |
MEDIA_ERROR | Camera/microphone access denied or unavailable |
TIMEOUT | Connection attempt timed out |
SERVER_ERROR | Server returned an unexpected error |
ROOM_NOT_FOUND | Room does not exist or was deleted |
ROOM_FULL | Broadcast room already has a publisher |
Examples
Live Streaming (Twitch-like)
A broadcast room with one publisher streaming to many subscribers.
Publisher (streamer page)
const lr = new LiveRelay('https://api.liverelay.dev'); const session = await lr.publish({ token: publishToken, video: true, audio: true }); // Toggle mute document.getElementById('mute-btn').onclick = () => { session.isAudioMuted ? session.unmuteAudio() : session.muteAudio(); }; // Stop stream document.getElementById('stop-btn').onclick = () => session.close();
Subscriber (viewer page)
const lr = new LiveRelay('https://api.liverelay.dev'); const session = await lr.subscribe({ token: subscribeToken, element: document.getElementById('player') });
Video Call (1:1)
const lr = new LiveRelay('https://api.liverelay.dev'); const session = await lr.call({ token: callToken, video: true, audio: true, element: document.getElementById('remote-video'), localElement: document.getElementById('local-video') });
Node.js Backend Integration
Example Express.js routes for creating rooms and distributing tokens to your users.
const LIVERELAY = 'https://api.liverelay.dev'; const API_KEY = process.env.LIVERELAY_API_KEY; // Create a broadcast room app.post('/api/go-live', async (req, res) => { const room = await fetch(`${LIVERELAY}/v1/rooms`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ room_type: 'broadcast' }) }).then(r => r.json()); // Store room in your database await db.streams.create({ userId: req.user.id, roomId: room.id, tokens: room.tokens }); res.json({ publishToken: room.tokens.publish }); }); // Get subscribe token for viewers app.get('/api/watch/:streamId', async (req, res) => { const stream = await db.streams.findById(req.params.streamId); res.json({ subscribeToken: stream.tokens.subscribe }); });
Error Codes
Complete reference of all error codes returned by the LiveRelay REST API and SFU endpoints.
| Code | Status | Message | Cause |
|---|---|---|---|
auth_header_missing |
401 | Authorization header is missing | No Bearer token in request |
api_key_invalid |
401 | API key is not valid | Wrong or revoked API key |
token_invalid |
401 | Token signature is invalid | Corrupted or wrong JWT |
token_expired |
401 | Token has expired | JWT past expiration time |
role_insufficient |
403 | Token role cannot perform this action | e.g., subscribe token used on /sfu/publish |
room_not_found |
404 | Room does not exist | Wrong room_id or room was deleted |
room_full |
409 | Room has reached publisher limit | Broadcast room already has a publisher |
no_publisher_available |
404 | Room has no active publisher | Trying to subscribe before anyone publishes |
invalid_role |
400 | Invalid role value | Role must be publish, subscribe, or call |
room_type_mismatch |
400 | Wrong room type for this endpoint | e.g., using /sfu/call on a broadcast room |
invalid_sdp |
400 | Malformed SDP offer | Invalid WebRTC session description |
peer_connection_failed |
500 | WebRTC connection setup failed | Server-side WebRTC error |
{"error": "code", "message": "Human-readable message"}