Join a Room over HTTP, without the SDK
Build a call client with no MediaSFU SDK at all. Your server adds each person to the room over HTTP; their app publishes with standard WHIP and receives other people with standard WHEP. Chat, polls, requests and media controls are HTTP calls too. This suits lightweight web clients, kiosks, embedded devices and server-side bots.
For a complete, working example across React, Angular, Flutter, Expo and Android, see the WHIP/WHEP apps in Familiar Calls.
How it works
- Your server creates a room and keeps its
meetingID. - For each person, your server creates an external session. MediaSFU returns a WHIP URL and token; the person's app publishes their microphone and camera to it.
- For each person they should see, your server creates a playback from that person's session. MediaSFU returns a WHEP URL and token; the app plays it.
- Chat, polls, requests and track controls go through your server as HTTP calls.
- When the call ends, your server deletes the playbacks, then the sessions.
People who use an SDK see HTTP participants like anyone else. Playbacks are created from external sessions, so HTTP participants receive other HTTP participants.
All requests below come from your server, with
Authorization: Bearer <api-username>:<api-key>. Your app never holds the API
key; it only receives the WHIP or WHEP URL and token for its own streams.
1. Add a participant
curl -X POST "https://mediasfu.com/v1/meetings/$MEETING_ID/external-sessions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY" \
-H "Idempotency-Key: call-42-user-7" \
-d '{"displayName":"Ada","role":"participant","ingest":{"protocol":"whip","tracks":["audio","video"]}}'
roleishostorparticipant.- Use
"tracks": ["audio"]for a voice-only call. - The
Idempotency-Keymakes retries safe: the same key returns the same session instead of adding the person twice.
The response carries sessionID and, under ingest, the url, token and
expiresAt the app needs. Send the app only ingest.url and ingest.token;
it publishes with any WHIP client, passing the token as a bearer token.
2. Wait until their media is live
curl "https://mediasfu.com/v1/meetings/$MEETING_ID/external-sessions/$SESSION_ID" \
-H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY"
The session is ready when state is active and its tracks include each kind
you asked for, none closed or failed. Before that, tell other people's apps
to wait and check again.
3. Let someone receive another participant
Create one playback per viewer and source:
curl -X POST "https://mediasfu.com/v1/meetings/$MEETING_ID/playbacks" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY" \
-H "Idempotency-Key: call-42-user-9-watches-user-7" \
-d "{\"sourceSessionID\":\"$SESSION_ID\",\"protocol\":\"whep\",\"tracks\":[\"audio\",\"video\"]}"
Send the viewer's app endpointUrl and playbackToken; it plays them with a
WHEP client. If the player gets 406 PAYLOAD_TYPE_MISMATCH, its offer doesn't
match the stream's payload profile; see Play with WHEP.
WHEP opens one connection per viewer and source, which suits calls and small groups. For a large audience, broadcast with HLS.
4. Control media
Get track IDs from the session's tracks.
| To | Request |
|---|---|
| Mute or unmute a track | POST …/external-sessions/:sessionID/tracks/:trackID/state with {"state":"paused"} or {"state":"active"} |
| Stop a track | POST …/external-sessions/:sessionID/tracks/:trackID/close |
| Ask for a fresh video frame (after a viewer joins or a picture freezes) | POST …/external-sessions/:sessionID/keyframe with {"trackID":"…"} |
… stands for https://mediasfu.com/v1/meetings/:meetingID.
5. Chat, polls and requests
Read what the participant should see (their media state, recent chat, polls, breakout and whiteboard assignment):
curl "https://mediasfu.com/v1/meetings/$MEETING_ID/external-sessions/$SESSION_ID/participant-state" \
-H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY"
Act on their behalf with POST …/external-sessions/:sessionID/participant-actions:
| Action | Body |
|---|---|
| Send a chat message to everyone | {"action":"chat.send","message":"Hello"} (1–300 characters) |
| Send a direct message | {"action":"chat.send","message":"Hi","receivers":["ada_7"]} (up to two participant names) |
| Vote in a poll | {"action":"poll.vote","pollID":"poll-001","choice":0} (choice 0–4) |
| Ask the host for permission | {"action":"participant.request","icon":"fa-microphone"}; also fa-video, fa-desktop (screen share) or fa-comments (chat) |
MediaSFU takes the sender from the session itself, so a request can't speak
for someone else. Poll participant-state for updates, or use an SDK when the
product needs instant push updates.
6. End the call
Delete every playback first, then each session:
curl -X DELETE "https://mediasfu.com/v1/meetings/$MEETING_ID/playbacks/$PLAYBACK_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY" \
-d '{"reason":"event_finished"}'
curl -X DELETE "https://mediasfu.com/v1/meetings/$MEETING_ID/external-sessions/$SESSION_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MEDIASFU_API_USERNAME:$MEDIASFU_API_KEY" \
-d '{"reason":"event_finished"}'
Repeating a delete is safe. Also clear the URLs and tokens from your own storage.
Related
- Streaming overview: OBS, WHEP players and HLS broadcasts.
- Streaming HTTP reference: every route and rule in one place.
- Rooms API: create, join and end rooms.