Skip to main content

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​

  1. Your server creates a room and keeps its meetingID.
  2. 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.
  3. 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.
  4. Chat, polls, requests and track controls go through your server as HTTP calls.
  5. 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"]}}'
  • role is host or participant.
  • Use "tracks": ["audio"] for a voice-only call.
  • The Idempotency-Key makes 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.

ToRequest
Mute or unmute a trackPOST …/external-sessions/:sessionID/tracks/:trackID/state with {"state":"paused"} or {"state":"active"}
Stop a trackPOST …/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:

ActionBody
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.