# MediaSFU: complete reference for AI coding tools > Generated from https://mediasfu.com/docs/usage/llm-reference and https://mediasfu.com/docs/usage/copy-paste-recipes. This page gives an AI coding assistant, or a developer in a hurry, everything needed to build a working MediaSFU integration from one context window. The same content is available as plain text at [`/docs/llms-full.txt`](https://mediasfu.com/docs/llms-full.txt). MediaSFU is real-time video, voice, AI agents, phone (SIP/PSTN) calls, recording, live translation and HLS in one platform. You call its HTTP API from your server and render rooms with an SDK (React, Angular, Vue, React Native, Expo, Flutter, Kotlin, Swift, Unity) or the `@mediasfu/widgets` web components. ## Ground rules - **Base URL:** `https://mediasfu.com`. Every API path starts with `/v1/`. - **Auth header:** `Authorization: Bearer :`. Both values are at [mediasfu.com/api-keys](https://mediasfu.com/api-keys). The key is 64 characters. - **Server only.** Call the API from your backend. Never put the API key in browser, mobile or desktop code. For a browser that must act directly, issue a short-lived, scoped [disposable key](https://mediasfu.com/docs/api-reference/http/disposablekeys). - **Environments:** a sandbox key creates sandbox rooms (IDs start with `s`), a production key creates production rooms (IDs start with `p`, billed). Start with sandbox. - **Actions:** each resource takes an `action` field: in the JSON body for `POST`, in the query string for `GET`. - **Responses:** success is HTTP 2xx with `success: true` and the payload beside it. Failure is `{ "success": false, "error": "…", "code": "NOT_FOUND" }`. Branch on `code`: `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `RATE_LIMITED`, `INTERNAL_ERROR`. - **Retries:** send an `Idempotency-Key` header (8–128 visible ASCII characters) on room create and join so a retry returns the same room. ## Rooms Everything happens in a room: meetings, webinars, broadcasts, phone calls and agent sessions. | Do | Request | | --- | --- | | Create | `POST /v1/rooms` `{"action":"create","userName":"host","duration":30,"capacity":5,"eventType":"conference"}` | | Join | `POST /v1/rooms` `{"action":"join","userName":"guest1","meetingID":"s1a2b3c4"}` | | List | `GET /v1/rooms?action=get&eventStatus=active&startIndex=0&pageSize=20` | | Get one | `GET /v1/rooms?action=get&meetingID=s1a2b3c4` | | Counts | `GET /v1/rooms/stats` | | End and remove | `POST /v1/rooms` `{"action":"delete","meetingID":"s1a2b3c4"}` | Create fields: | Field | Required | Values | | --- | --- | --- | | `userName` | yes | Host name, unique among your active rooms | | `duration` | yes | Minutes, 5–1440 | | `capacity` | yes | Maximum participants | | `eventType` | yes | `conference`, `webinar`, `broadcast`, `chat` | | `scheduledDate` | no | ISO 8601 start time | | `secureCode` | no | Host secret; generated if omitted | | `recordOnly` | no | `true` for a recording-only room | | `recordingParams` | no | `recordingAudioSupport`, `recordingVideoSupport`, `recordingHLSSupport`, `recordingPreferredOrientation` | | `meetingRoomParams` | no | `audioSetting`, `videoSetting`, `screenshareSetting` (`allow`/`approval`/`disallow`), `chatSetting`, `targetResolution` (`QnHD`, `sd`, `hd`, `fhd`, `qhd`), `addCoHost` | | `supportSIP`, `directionSIP` | no | Let phone callers into the room: `true`; `inbound`, `outbound` or `both` | | `webhookUrl`, `webhookSecret` | no | Webhook for this room only (see [Webhooks](#webhooks)) | A successful create returns `roomName`, `meetingID`, `secureCode` and `publicURL`. Send people to `publicURL` for a ready-made room, or pass the create/join result to an SDK. Use `QnHD` resolution for low-bandwidth audiences (rural clinics, mobile data). ## Show the room in your app React (`mediasfu-reactjs`). The browser calls your backend; your backend adds the API key and forwards the same payload to `POST https://mediasfu.com/v1/rooms`. ```tsx import { ModernMediasfuGeneric } from 'mediasfu-reactjs'; import type { CreateRoomOnMediaSFUType, JoinRoomOnMediaSFUType } from 'mediasfu-reactjs'; const viaBackend = (path: string) => async ({ payload }: { payload: unknown }) => { const response = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!response.ok) throw new Error(`MediaSFU request failed: ${response.status}`); return response.json(); }; export function Meeting() { return ( ); } ``` The backend routes are in [Copy-paste recipes](https://mediasfu.com/docs/usage/copy-paste-recipes) and [Secure backend](https://mediasfu.com/docs/usage/secure-backend-proxy). Other SDKs: [SDK guides](https://mediasfu.com/docs/sdks). No SDK at all: [participants over HTTP (WHIP/WHEP)](https://mediasfu.com/docs/usage/http-participant). ## AI agents and phone calls 1. **Add a number.** Bring your own SIP number with `POST /v1/sipconfigs` `{"action":"create","contactNumber":"+15551234567","provider":"twilio","supportSipActive":true,"supportSipNameCalls":true,"allowOutgoing":true}`. Attach an AI agent to the number in the dashboard (Agents). MediaSFU-provided numbers are for outgoing calls without an agent. 2. **Inbound:** callers to that number reach the attached agent. No code needed. 3. **Outbound:** a host must be connected in a live room created with `"supportSIP": true`. Then: ```bash curl -X POST https://mediasfu.com/v1/sipcall/outgoingCall \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY" \ -d '{"calledDid":"+15557654321","callerIdNumber":"+15551234567","roomName":"s1a2b3c4","initiatorName":"host","callPurpose":"Confirm tomorrow 10am appointment","callObjective":"Confirmed or rescheduled"}' ``` `callerIdNumber` is your SIP number (E.164). `initiatorName` is alphanumeric and must be the name of someone already in the room. Optional context for the agent: `callPurpose` (≤500), `callObjective` (≤300), `referenceId` (≤100), `urgency` (`low`/`normal`/`high`/`urgent`), `callScript` (≤2000), `customData` (up to 20 keys). 4. **Control a live call** with its `sipCallId`: | Do | Request | | --- | --- | | Read state | `GET /v1/sipcall/:sipCallId/state` | | Start or stop the agent | `POST /v1/sipcall/:sipCallId/start-agent` or `/stop-agent` | | Hand to a person or back | `POST /v1/sipcall/:sipCallId/switch-source` `{"targetType":"human","humanName":"Pat"}` | | Hold, unhold | `POST /v1/sipcall/:sipCallId/hold`, `/unhold` | | End | `POST /v1/sipcall/:sipCallId/end` | | List calls | `GET /v1/sipcall/list?page=1&limit=20&direction=incoming` | | Totals | `GET /v1/sipcall/summary` | A `429` carries `retryAfterMs`; wait that long before the next control. ## Recordings Turn recording on with `recordingParams` at create (or from the room UI). After `recording.completed` arrives, list files: `GET /v1/recordings?action=get&name=` returns `recordings[]` with `url`, `size` and `duration`. Without `name` it pages through all recordings (`startIndex`, `pageSize`). ## Webhooks **Room events.** Set an account default with `POST /v1/eventssettings` `{"action":"update","webhookUrl":"https://example.com/hooks/mediasfu","webhookSecret":"…"}` (use `"action":"create"` the first time, with `type`, `audioSetting` and `videoSetting`), or per room with `webhookUrl` on create. The URL must be public HTTPS with no port, credentials or fragment. Send an empty string to clear. MediaSFU POSTs JSON: ```json { "id": "6f1c…", "event": "room.ended", "roomName": "s1a2b3c4", "timestamp": "2026-10-13T18:00:00.000Z", "data": {} } ``` Events: `room.created`, `room.ended`, `participant.joined`, `participant.left`, `recording.started`, `recording.paused`, `recording.resumed`, `recording.stopped`, `recording.processing`, `recording.completed`, `recording.failed`, `meeting.expired`, `translation.started`. Headers: `x-webhook-event`, `x-webhook-id`, `x-webhook-timestamp` and, when a secret is set, `x-webhook-signature` = lowercase hex HMAC-SHA256 of the raw body. Return 2xx quickly; a 5xx is retried three times (2 s, 8 s, 30 s). Use `id` to ignore duplicates. ```js import crypto from 'node:crypto'; export function verifyMediaSfuWebhook(rawBody, signature, secret) { const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); return typeof signature === 'string' && signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } ``` **Call events.** Set `webhookUrl` on the SIP number's config in the dashboard. MediaSFU POSTs `eventType` `sip:callStateUpdate` (with `sipCallId`, `roomName`, `calledDid`, `currentState.status`, `currentState.durationSeconds`, `currentState.callEnded`), `sip:conversationSummary` (summary text and transcript), `sip:humanInterventionRequired` and `sip:callbackRequested`. A call has ended when `currentState.callEnded` is `true`. ## Usage and balance | Do | Request | | --- | --- | | Remaining minutes | `GET /v1/balance` (optional `scope=sandbox` or `production`) | | One room's itemised usage | `GET /v1/room/logs?roomName=&fieldKey=usageLogs&envelope=1` | | One room's recording usage | same, with `fieldKey=recordUsageLogs` | Each usage entry records the interval with producers, viewers, quality and rate. Prices: [mediasfu.com/pricing](https://mediasfu.com/pricing). ## Common mistakes | Symptom | Cause | Fix | | --- | --- | --- | | `401` on every call | Header is `Bearer ` only, or username and key swapped | Send `Bearer :` | | `401` "demo key" | Demo keys can't create rooms over the API | Use a sandbox or production key | | `401` with a production key | Called over `http` | Use `https://mediasfu.com` | | Production create rejected from a hosted app | The calling domain isn't allowed | Add it with [`/v1/domains`](https://mediasfu.com/docs/api-reference/http/domains) | | Outbound call `404` "Initiator not found" | Nobody named `initiatorName` is in the room | Join the room as that name first | | Outbound call `400` "does not support SIP" | Room created without `supportSIP` | Create with `"supportSIP": true` | | Webhook URL rejected | Not public HTTPS, or has a port | Use a public HTTPS hostname on 443 | | API key in browser network tab | Frontend calls MediaSFU directly | Route through your backend or use a disposable key | ## Use MediaSFU from an AI assistant Give your assistant MediaSFU knowledge and account tools with the MCP server: ```bash npx -y @mediasfu/mcp-sdk-tools ``` Account actions (rooms, calls, recordings, webhooks, usage) use your own key from the client's environment. Setup: [MediaSFU MCP](https://mediasfu.com/docs/usage/mcp). ## More - [Copy-paste recipes](https://mediasfu.com/docs/usage/copy-paste-recipes): working server code for each flow above. - [HTTP API reference](https://mediasfu.com/docs/api-reference/http): every field of every resource. - [AI call control](https://mediasfu.com/docs/operations/ai-call-control-http): operator handoff patterns. - [Starter projects](https://mediasfu.com/docs/starters): runnable apps. --- Each recipe is complete server code you can paste into a project. They all read two environment variables, `MEDIASFU_API_USERNAME` and `MEDIASFU_API_KEY`, from [mediasfu.com/api-keys](https://mediasfu.com/api-keys). Use a sandbox key while you build. ## 1. A tiny MediaSFU client (Node 18+) Every other recipe uses this helper. ```js // mediasfu.mjs const BASE = 'https://mediasfu.com'; export async function mediasfu(method, path, { query, body, idempotencyKey } = {}) { const url = new URL(path, BASE); for (const [key, value] of Object.entries(query ?? {})) { if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, String(value)); } const response = await fetch(url, { method, headers: { Authorization: `Bearer ${process.env.MEDIASFU_API_USERNAME}:${process.env.MEDIASFU_API_KEY}`, Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}), ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}), }, ...(body ? { body: JSON.stringify(body) } : {}), }); const data = await response.json().catch(() => ({})); if (!response.ok || data.success === false) { const error = new Error(data.error ?? `MediaSFU ${method} ${path} failed (${response.status})`); error.status = response.status; error.code = data.code; throw error; } return data; } ``` Check it works: `node -e "import('./mediasfu.mjs').then(m => m.mediasfu('GET','/v1/balance')).then(console.log)"` prints your remaining minutes. ## 2. Create a room and share the link ```js import { mediasfu } from './mediasfu.mjs'; const room = await mediasfu('POST', '/v1/rooms', { idempotencyKey: 'standup-2026-10-14', body: { action: 'create', userName: 'host', duration: 30, capacity: 10, eventType: 'conference', meetingRoomParams: { targetResolution: 'QnHD' }, }, }); console.log(room.meetingID, room.publicURL); ``` Open `publicURL` in two browsers: both people see and hear each other. ## 3. Next.js: backend routes for the React SDK `app/api/mediasfu/[op]/route.ts` serves both `create-room` and `join-room`. The browser never sees the API key. ```ts import { NextResponse } from 'next/server'; const ALLOWED = new Set(['create-room', 'join-room']); export async function POST(request: Request, { params }: { params: Promise<{ op: string }> }) { const { op } = await params; if (!ALLOWED.has(op)) return NextResponse.json({ error: 'Not found' }, { status: 404 }); // Authenticate your user here and decide whether they may create or join. const payload = await request.json(); if (op === 'create-room') payload.action = 'create'; if (op === 'join-room') payload.action = 'join'; const upstream = await fetch('https://mediasfu.com/v1/rooms/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MEDIASFU_API_USERNAME}:${process.env.MEDIASFU_API_KEY}`, }, body: JSON.stringify(payload), }); const data = await upstream.json().catch(() => ({})); return upstream.ok ? NextResponse.json({ success: true, data }) : NextResponse.json({ success: false, error: data.error ?? 'Room request failed' }, { status: upstream.status }); } ``` Render the room with the component in the [AI tools reference](https://mediasfu.com/docs/usage/llm-reference#show-the-room-in-your-app). ## 4. Receive and verify webhooks (Express) ```js import crypto from 'node:crypto'; import express from 'express'; import { mediasfu } from './mediasfu.mjs'; const app = express(); app.post('/hooks/mediasfu', express.raw({ type: 'application/json' }), async (req, res) => { const secret = process.env.MEDIASFU_WEBHOOK_SECRET; const signature = req.get('x-webhook-signature') ?? ''; const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex'); const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!valid) return res.sendStatus(401); const event = JSON.parse(req.body.toString('utf8')); res.sendStatus(204); // acknowledge first; MediaSFU retries 5xx responses if (event.event === 'recording.completed') { const { recordings } = await mediasfu('GET', '/v1/recordings', { query: { action: 'get', name: event.roomName }, }); console.log('Recording ready:', recordings?.[0]?.url); } if (event.event === 'room.ended') console.log('Room ended:', event.roomName); }); app.listen(3000); ``` Register the URL once (use a public HTTPS tunnel while developing): ```js await mediasfu('POST', '/v1/eventssettings', { body: { action: 'update', webhookUrl: 'https://your-app.example.com/hooks/mediasfu', webhookSecret: process.env.MEDIASFU_WEBHOOK_SECRET, }, }); ``` If you have never saved event settings, send `action: 'create'` with `type: 'conference'`, `audioSetting: 'allow'` and `videoSetting: 'allow'`. ## 5. Place an AI agent phone call Prerequisites: a SIP number with an agent attached, and a live room created with `supportSIP: true` in which the host (`initiatorName`) is connected. ```js import { mediasfu } from './mediasfu.mjs'; const call = await mediasfu('POST', '/v1/sipcall/outgoingCall', { body: { calledDid: '+15557654321', callerIdNumber: process.env.MEDIASFU_SIP_NUMBER, roomName: 's1a2b3c4', initiatorName: 'host', callPurpose: 'Confirm tomorrow 10am dental appointment', callObjective: 'Confirmed, or a new time agreed', referenceId: 'appt-8812', }, }); console.log(call); ``` Hand the call to a person, or end it, with its `sipCallId`: ```js await mediasfu('POST', `/v1/sipcall/${encodeURIComponent(sipCallId)}/switch-source`, { body: { targetType: 'human', humanName: 'Pat' }, }); await mediasfu('POST', `/v1/sipcall/${encodeURIComponent(sipCallId)}/end`, { body: { reason: 'Done' }, }); ``` ## 6. Know when a phone call ended Set the call webhook on the number's SIP config in the dashboard, then: ```js app.post('/hooks/calls', express.json(), (req, res) => { res.sendStatus(204); const update = req.body; if (update.eventType === 'sip:callStateUpdate' && update.currentState?.callEnded) { console.log('Call ended', update.sipCallId, update.currentState.durationSeconds, 's'); } if (update.eventType === 'sip:conversationSummary') { console.log('Summary', update.summary?.content); } }); ``` ## 7. Read usage and balance ```js const balance = await mediasfu('GET', '/v1/balance'); const usage = await mediasfu('GET', '/v1/room/logs', { query: { roomName: 's1a2b3c4', fieldKey: 'usageLogs', envelope: 1 }, }); console.log(balance, usage.data?.length ?? usage); ``` ## Common problems | Symptom | Fix | | --- | --- | | `UNAUTHORIZED` | Header must be `Bearer :`; demo keys can't create rooms | | Webhook never arrives | URL must be public HTTPS on port 443; check event settings saved with `GET /v1/eventssettings?action=get` | | Signature never matches | Hash the raw body bytes, before any JSON parsing | | Outbound call "Initiator not found" | Join the room as `initiatorName` before calling | More detail: [Reference for AI tools](https://mediasfu.com/docs/usage/llm-reference) and the [HTTP API reference](https://mediasfu.com/docs/api-reference/http).