# Metered Global Cloud SFU -- llms.txt reference (last updated: 2026-05-05) > Metered Global Cloud SFU is a globally distributed Selective Forwarding Unit that acts as a PUB-SUB service for audio, video, and data channels. It uses plain WebRTC API and HTTP REST calls -- no third-party SDK required. Users are automatically geo-routed to the nearest SFU for lowest latency. ## Overview Metered Global Cloud SFU lets you build any real-time application involving audio, video, or data channels. Participants publish media tracks to the SFU and subscribe to tracks from other participants. Users auto-connect to the nearest SFU by geography; media between regions routes over high-speed inter-SFU links. **Connection Flow:** 1. **Establish Connection:** Create `peerConnection` (native WebRTC API), send SDP offer to SFU via HTTP, set returned SDP answer on `peerConnection`. 2. **Publish a Track:** Add track to `peerConnection`, generate new SDP offer, send to Publish Track API, set returned SDP answer. 3. **Subscribe to a Track:** Call Subscribe Track API with `remoteSessionId` + `remoteTrackId`, set returned SDP offer as remote description, create answer, call Renegotiate API. The `ontrack` event fires. **Key Advantages:** Scalable (media sent once to SFU, forwarded to all subscribers); platform independent (native WebRTC + HTTP, no SDK); flexible pub-sub (no rooms); cost efficient (billed by GB downloaded, uploads free). ## Getting Started 1. **Sign up** at https://dashboard.metered.ca/signup to create a free Metered account. 2. Go to **Dashboard → Global SFU** and click **"Add SFU App"** to create an SFU application. 3. After creating the app, note your **SFU App ID** and **Secret** shown on the dashboard. You need both for all API calls. 4. The **SFU App ID** goes in the URL path. The **Secret** goes in the `Authorization: Bearer {secret}` header. 5. You can create multiple SFU apps for different environments (dev/staging/prod) or per-tenant isolation. ## Key Terms - **SFU Application (App):** Container for sessions. Has `SFU_APP_ID` and `SFU_APP_SECRET`. Create from Dashboard → Global SFU. Use to scope dev/staging/prod or per-tenant. - **Session:** Represents a single `peerConnection` to the SFU. Scoped within an App. Created via Create Session API which returns a `session_id` and remote SDP. - **Track:** An audio or video stream published to the SFU. Has `trackId`, `mid` (SDP media line ID), optional `customTrackName`, and `trackKind` (video/audio). Multiple tracks per session. - **No Rooms:** There is no room concept. Publish and subscribe freely within an App. ## Authentication All API requests require a Bearer token in the `Authorization` header. The token is the `Secret` of your SFU App (found in Dashboard → Global SFU). ``` Authorization: Bearer {sfu_app_secret} Content-Type: application/json ``` ## Base URL ``` https://global.sfu.metered.ca/api/sfu/:sfu_app_id/ ``` Replace `:sfu_app_id` with your SFU Application ID. STUN server for peerConnection (no TURN needed): ``` stun:stun.metered.ca:80 ``` ## Endpoint Summary | Method | Path | Description | |--------|------|-------------| | POST | `/api/sfu/:sfu_app_id/session/new` | Create a new session | | POST | `/api/sfu/:sfu_app_id/session/:session_id/track/publish` | Publish track(s) | | POST | `/api/sfu/:sfu_app_id/session/:session_id/track/subscribe` | Subscribe to track(s) | | POST | `/api/sfu/:sfu_app_id/session/:session_id/track/close` | Close a track | | GET | `/api/sfu/:sfu_app_id/sessions` | List active sessions | | GET | `/api/sfu/:sfu_app_id/session/:session_id/tracks` | List tracks for a session | | PUT | `/api/sfu/:sfu_app_id/session/:session_id/renegotiate` | Renegotiate session SDP | All endpoints require headers: `Authorization: Bearer {secret}` and `Content-Type: application/json`. --- ## REST API Endpoints ### Create Session Creates a new session representing a `peerConnection`. ``` POST /api/sfu/:sfu_app_id/session/new ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID from the dashboard. | **Request Body** ```json { "sessionDescription": "", "metadata": "optional descriptive text" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `sessionDescription` | object | Yes | The SDP offer from the local `peerConnection`. | | `metadata` | string | No | Custom text to store additional info about the session. | **Response** ```json { "sessionDescription": { "type": "answer", "sdp": "v=0\r\no=- 8184802762297966397 ..." }, "sessionId": "session-101-fc756623-3be4-4c40-b651-20e22095ee07" } ``` | Field | Type | Description | |-------|------|-------------| | `sessionDescription` | object | Remote SDP answer. Set as remote description on `peerConnection`. | | `sessionId` | string | Unique session identifier. Used in all subsequent API calls. | --- ### Publish Track Publishes one or more tracks through an existing session. Add track(s) to `peerConnection`, generate new SDP offer, then call this endpoint. ``` POST /api/sfu/:sfu_app_id/session/:session_id/track/publish ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID. | | `session_id` | string | Session ID from Create Session. | **Request Body** ```json { "tracks": [ { "trackId": "local-track-uuid", "mid": "1", "customTrackName": "userA-video" } ], "sessionDescription": "" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `tracks` | array | Yes | Array of track objects to publish. | | `sessionDescription` | object | Yes | New SDP offer after adding tracks to peerConnection. | Each object in `tracks`: | Field | Type | Required | Description | |-------|------|----------|-------------| | `trackId` | string | Yes | Track ID from `transceiver.sender.track.id`. | | `mid` | string | Yes | Media line ID from `transceiver.mid`. | | `customTrackName` | string | No | Custom name to identify the track. | **Response** ```json { "sessionDescription": { "type": "answer", "sdp": "v=0\r\no=- 7170991192596339755 ..." }, "sessionId": "session-101-03d73157-9df4-4bfc-91c9-01647bcfe80c" } ``` | Field | Type | Description | |-------|------|-------------| | `sessionDescription` | object | Remote SDP answer. Set as remote description on `peerConnection`. | | `sessionId` | string | The session ID. | **Code Example (publish a track to an existing session):** ```javascript // Add a track to the existing peerConnection const stream = await navigator.mediaDevices.getUserMedia({ video: true }); const track = stream.getVideoTracks()[0]; const transceiver = peerConnection.addTransceiver(track, { direction: "sendonly" }); const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); const response = await fetch( `https://global.sfu.metered.ca/api/sfu/${sfuAppId}/session/${sessionId}/track/publish`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sfuSecret}` }, body: JSON.stringify({ tracks: [{ trackId: track.id, mid: transceiver.mid, customTrackName: "my-video" }], sessionDescription: peerConnection.localDescription }) } ); const data = await response.json(); await peerConnection.setRemoteDescription(data.sessionDescription); ``` --- ### Subscribe Track Subscribes to one or more tracks published by another session. After calling this, you must complete renegotiation (see steps below). ``` POST /api/sfu/:sfu_app_id/session/:session_id/track/subscribe ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID. | | `session_id` | string | Session ID of the subscribing session (your session). | **Request Body** ```json { "tracks": [ { "remoteSessionId": "session-101-abc...", "remoteTrackId": "track-uuid..." } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `tracks` | array | Yes | Array of objects identifying remote tracks to subscribe to. | Each object in `tracks`: | Field | Type | Required | Description | |-------|------|----------|-------------| | `remoteSessionId` | string | Yes | Session ID of the publisher. | | `remoteTrackId` | string | Yes | Track ID of the remote track. | **Response** ```json { "immediateRenegotiationRequired": true, "sessionDescription": { "type": "offer", "sdp": "v=0\r\no=- 7684479339383422378 ..." }, "sessionId": "session-101-5666e439-6660-414b-adc1-050ed4e8d666" } ``` | Field | Type | Description | |-------|------|-------------| | `immediateRenegotiationRequired` | boolean | If true, you must call Renegotiate API after setting remote description and creating an answer. | | `sessionDescription` | object | Remote SDP offer. Set as remote description, then create an answer. | | `sessionId` | string | The session ID. | **Post-subscribe renegotiation steps:** 1. Set returned `sessionDescription` as remote description on `peerConnection`. 2. Create answer SDP via `peerConnection.createAnswer()`. 3. Set answer as local description. 4. Call Renegotiate API with the answer SDP. --- ### Close Track Stops publishing a track. Remove the track from `peerConnection`, generate new SDP, send with track ID. ``` POST /api/sfu/:sfu_app_id/session/:session_id/track/close ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID. | | `session_id` | string | Session ID that published the track. | **Request Body** ```json { "tracks": [ { "trackId": "track-uuid-to-close" } ], "sessionDescription": "" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `tracks` | array | Yes | Array of track objects to close. Each must have `trackId`. | | `sessionDescription` | object | Yes | New SDP offer after removing the track from peerConnection. | **Response** ```json { "sessionId": "session-101-...", "success": true, "sessionDescription": { "type": "answer", "sdp": "..." } } ``` | Field | Type | Description | |-------|------|-------------| | `sessionId` | string | The session ID. | | `success` | boolean | Whether the track was closed successfully. | | `sessionDescription` | object | Remote SDP answer. Set as remote description on `peerConnection`. | **Code Example (close a published track):** ```javascript const sender = peerConnection.getSenders().find(s => s.track && s.track.id === trackId); peerConnection.removeTrack(sender); const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); const response = await fetch( `https://global.sfu.metered.ca/api/sfu/${sfuAppId}/session/${sessionId}/track/close`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sfuSecret}` }, body: JSON.stringify({ tracks: [{ trackId: trackId }], sessionDescription: peerConnection.localDescription }) } ); const data = await response.json(); await peerConnection.setRemoteDescription(data.sessionDescription); ``` --- ### Fetch Sessions Returns all active sessions within an SFU App. ``` GET /api/sfu/:sfu_app_id/sessions ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID. | No request body. **Response** ```json [ { "connectionState": "connected", "metadata": "", "sessionId": "session-101-...", "sfuAppId": "66ad57c7..." } ] ``` | Field | Type | Description | |-------|------|-------------| | `connectionState` | string | Connection state (e.g., "connected"). | | `metadata` | string | Custom metadata from session creation. | | `sessionId` | string | Unique session identifier. | | `sfuAppId` | string | The SFU App ID. | --- ### Fetch Tracks Returns all active tracks for a given session. ``` GET /api/sfu/:sfu_app_id/session/:session_id/tracks ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID. | | `session_id` | string | Session ID to fetch tracks for. | No request body. **Response** ```json [ { "sessionId": "session-101-...", "sfuAppId": "66ad57c7...", "trackId": "33f140fc-...", "customTrackName": "userA-video", "mid": "1", "trackKind": "video" } ] ``` | Field | Type | Description | |-------|------|-------------| | `sessionId` | string | Session that published this track. | | `sfuAppId` | string | The SFU App ID. | | `trackId` | string | Unique track identifier. Use when subscribing. | | `customTrackName` | string | Custom name assigned during publish. | | `mid` | string | Media line identifier from the SDP. | | `trackKind` | string | `"video"` or `"audio"`. | --- ### Renegotiate Updates SDP for an existing session. Required after subscribing to a track to send the answer SDP back. ``` PUT /api/sfu/:sfu_app_id/session/:session_id/renegotiate ``` **URL Parameters** | Parameter | Type | Description | |-----------|------|-------------| | `sfu_app_id` | string | Your SFU App ID. | | `session_id` | string | Session ID to renegotiate. | **Request Body** ```json { "sessionDescription": "" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `sessionDescription` | object | Yes | The updated SDP (typically an answer) from `peerConnection`. | **Response:** HTTP 200 on success. --- ## Quick Start Example Publishes a webcam track (User A) and subscribes from a second session (User B). ```javascript (async () => { const host = "https://global.sfu.metered.ca"; const sfuAppId = ""; const secret = ""; // PUBLISHER (User A): create peerConnection, get webcam, create session const pcA = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.metered.ca:80" }] }); const stream = await navigator.mediaDevices.getUserMedia({ video: true }); stream.getTracks().forEach(track => pcA.addTrack(track, stream)); const offerA = await pcA.createOffer(); await pcA.setLocalDescription(offerA); const resA = await fetch(`${host}/api/sfu/${sfuAppId}/session/new`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` }, body: JSON.stringify({ sessionDescription: offerA }) }); const jsonA = await resA.json(); const sessionIdA = jsonA.sessionId; await pcA.setRemoteDescription(jsonA.sessionDescription); // Wait for connection await new Promise(r => { pcA.oniceconnectionstatechange = () => { if (pcA.iceConnectionState === 'connected') r(); }; }); // SUBSCRIBER (User B): create peerConnection, create session const pcB = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.metered.ca:80" }] }); pcB.addTransceiver('video'); pcB.ontrack = (e) => { const video = document.createElement('video'); video.srcObject = new MediaStream([e.track]); video.autoplay = true; document.body.appendChild(video); }; const offerB = await pcB.createOffer(); await pcB.setLocalDescription(offerB); const resB = await fetch(`${host}/api/sfu/${sfuAppId}/session/new`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` }, body: JSON.stringify({ sessionDescription: offerB }) }); const jsonB = await resB.json(); const sessionIdB = jsonB.sessionId; await pcB.setRemoteDescription(jsonB.sessionDescription); // Fetch tracks from User A, then subscribe const tracksRes = await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdA}/tracks`, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` } }); const tracks = await tracksRes.json(); const trackId = tracks[0].trackId; const subRes = await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdB}/track/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` }, body: JSON.stringify({ tracks: [{ remoteSessionId: sessionIdA, remoteTrackId: trackId }] }) }); const subJson = await subRes.json(); // Renegotiate to complete subscription await pcB.setRemoteDescription(new RTCSessionDescription(subJson.sessionDescription)); const answer = await pcB.createAnswer(); await pcB.setLocalDescription(answer); await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdB}/renegotiate`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` }, body: JSON.stringify({ sessionDescription: answer }) }); // pcB.ontrack fires with User A's video })(); ``` ### Flow Summary 1. **Create SFU App** in the Metered dashboard. Note `SFU_APP_ID` and `SECRET`. 2. **Create session** for publisher (`POST /session/new` with SDP offer). 3. **Publish tracks** (`POST /session/:id/track/publish` with tracks array + SDP). 4. **Create session** for subscriber (`POST /session/new` with SDP offer). 5. **Fetch tracks** from publisher's session (`GET /session/:id/tracks`). 6. **Subscribe** (`POST /session/:id/track/subscribe` with remoteSessionId + remoteTrackId). 7. **Renegotiate** (`PUT /session/:id/renegotiate` with answer SDP). 8. Subscriber's `ontrack` event fires with remote media track.