Copy-Paste Recipes for the MediaSFU API
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. Use a sandbox key while
you build.
1. A tiny MediaSFU client (Node 18+)
Every other recipe uses this helper.
// 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
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.
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.
4. Receive and verify webhooks (Express)
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):
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.
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:
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:
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
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 <api-username>:<api-key>; 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 and the HTTP API reference.