I have to admit something. The first time I read WHIP and WHEP, I thought it was a typo. Then I was sure they were a pair of cartoon chipmunks, Chip and Dale chattering away in an old Disney short. It took me an embarrassingly long third guess to accept that these are two real protocols, from the IETF, that I would actually want to use.

WHIP and WHEP, as far as I am concerned. We never know who is who 🙂
They are worth the silly names. WHIP and WHEP finally answer a question WebRTC left open for years: how do you get a stream into a server, and back out to a viewer, without inventing your own signaling protocol every single time. So it felt like a good moment to sit down with them and work out what they are for, how you use them, and which problem they actually solve.
WHIP itself was specified by Sergio Garcia Murillo, of Millicast (now part of Dolby), and Alex Gouaillard, of CoSMo Software. Their proposal was taken up by the IETF WISH working group (WebRTC Ingest Signaling over HTTPS) and published as RFC 9725 in 2025. Alex is a name many in the WebRTC world will recognize; he passed away before the RFC was finished.
To keep this concrete, and not just a reading of the specs, I built something. A small player that receives a live stream from a publisher through a Node server I wrote. Worth stressing up front: the WHIP and WHEP endpoints are just an HTTP door, a separate concern from the media server that sits behind them. In a real deployment those are often two different services, a signaling front end and a media backend. In my demo I fold them into one Node.js app, the REST endpoints and the node-datachannel media peer living in the same process, purely to keep the example small. And then, to see how it integrates with a streaming tool like OBS, I pointed it at my server over WHIP and watched its screen share appear in my browser player. If a tool I did not write can talk to a server I did write, the standard is doing its job.
Two HTTP-based signaling protocols from the IETF WISH group. WHIP, the WebRTC-HTTP Ingestion Protocol (RFC 9725), pushes media into a server. WHEP, the WebRTC-HTTP Egress Protocol (still an IETF draft), pulls it back out.
To see what they are for, it helps to remember what WebRTC deliberately left out. The WHIP RFC says it plainly:
The IETF RTCWEB Working Group standardized the JavaScript Session Establishment Protocol (JSEP), a mechanism used to control the setup, management, and teardown of a multimedia session. It also describes how to negotiate media flows using the offer/answer model with the Session Description Protocol (SDP) … WebRTC intentionally does not specify a signaling transport protocol at the application level.
Unfortunately, the lack of a standardized signaling mechanism in WebRTC has been an obstacle to its adoption as an ingestion protocol within the broadcast and streaming industry, where a streamlined production pipeline is taken for granted.
So WebRTC standardized the conversation, the offer, the answer, the SDP, but not the phone line that carries it. For a peer-to-peer app that is fine: you invent a little channel to shuttle the offer and answer across, and you move on. And invent we did. Some stacks carried the session over XMPP with Jingle, others ran SIP over WebSockets to reach the telephony world, and most products simply wrapped the SDP in a JSON envelope on a WebSocket and called it a day. They all did the same job, and none of them talked to each other.
WHIP and WHEP replace that missing phone line. They put the offer and answer dialog on a single, well-defined protocol, so an encoder and a server that have never met can still talk.
But does this apply to every WebRTC scenario, peer-to-peer included? In short, no. WHIP and WHEP are not trying to standardize how two browsers set up a call between themselves. They target the case where one side is a server, and here is the shift that matters: that server is not a signaling relay passing SDP between browsers. It is a WebRTC endpoint in its own right, a real peer that terminates ICE, DTLS, and SRTP and owns the media.
That is the reason they exist. WebRTC grew up peer-to-peer, browser to browser, but it keeps being pulled into places it was never designed for: live streaming, ingest pipelines, even remote-controlled robots. WHIP and WHEP are there to make WebRTC easy to adopt in the broadcast and streaming industry. That is not my framing, it is the objective the RFC sets out, and they meet it by shipping the one part WebRTC never did, a standard way to check in to a server.
And WHEP is not just WHIP in reverse. WHIP standardizes getting media in; WHEP standardizes getting it out, so one generic player can consume any WHEP server, the way a single HLS or DASH player handles any stream. That is what the draft keeps aiming at: reusable, interoperable players instead of a bespoke one per service.
Once the server is an endpoint, the three roles, publisher, server, and player, are cleanly decoupled. The publisher only knows there is a WHIP URL. The player only knows there is a WHEP URL. The server sits in the middle and owns the media, and you can swap any corner, a browser publisher for OBS, one player for a thousand, without touching the others.
The setup itself stays refreshingly small, and it is plain REST.
And the shape fits a whole family of jobs:
Here is the first half of the demo: a browser publisher sends its camera and microphone to a small Node server, and browser viewers subscribe and play it back. The server is not a message router shuffling SDP between browsers. It is a full WebRTC peer that terminates the media and forwards the RTP to everyone watching. One dependency, node-datachannel.
The publisher speaks WHIP. It captures, makes an offer, POSTs it, applies the answer, and starts sending. That is the whole client.
const media = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });const pc = new RTCPeerConnection();pc.addTransceiver(media.getAudioTracks()[0], { direction: 'sendonly' });pc.addTransceiver(media.getVideoTracks()[0], { direction: 'sendonly' });await pc.setLocalDescription(await pc.createOffer());await iceGatheringComplete(pc); // POST a complete offer, skip trickleconst res = await fetch('/whip', {method: 'POST',headers: { 'Content-Type': 'application/sdp' },body: pc.localDescription.sdp,});await pc.setRemoteDescription({ type: 'answer', sdp: await res.text() });
The server is where the real work happens. During signaling it just takes the offer, lets libdatachannel build the answer, and returns 201 with a Location. Once media flows, it reads the publisher’s RTP and writes it to every viewer. Here is how it is put together:
POST /whip, POST /whep, PATCH and DELETE /resource/:id), the static clients, /stats, plus CORS and optional bearer auth.node-datachannel peer and returns 201 with a Location. WHIP builds the publisher peer, WHEP builds one peer per viewer.a=ssrc, and boost the encoder bitrate.PATCH and DELETE, and cleanup when a peer drops.The player speaks WHEP, which is the same conversation with the arrows reversed. It declares what it wants to receive, POSTs its offer, and attaches whatever arrives to a <video>.
const pc = new RTCPeerConnection();pc.addTransceiver('audio', { direction: 'recvonly' });pc.addTransceiver('video', { direction: 'recvonly' });pc.ontrack = (e) => videoEl.srcObject.addTrack(e.track);await pc.setLocalDescription(await pc.createOffer());await iceGatheringComplete(pc);const res = await fetch('/whep', {method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp,});await pc.setRemoteDescription({ type: 'answer', sdp: await res.text() });
Read those two clients side by side and they are almost the same file. That symmetry is the whole promise of WHIP and WHEP.
One honest shortcut in that player: it only handles the happy 201 answer. WHEP also lets the server reject your offer with a 406 Not Acceptable and hand back its own counter-offer, which the player then answers with a PATCH. A player you can point at any WHEP server, Cloudflare, Dolby, MediaMTX, should implement that path too. Mine skips it, because my own relay always accepts the offer and answers 201.
Here is the honest surprise of the project. Using the protocols is trivial. The signaling is a POST, an answer, and a Location header, and I had the publisher and player talking to the server in an afternoon.
Everything hard lived in the server, in making a passthrough relay hand a real browser media it would actually accept. That is ordinary WebRTC plumbing, the work an SFU already does for you, and none of it is in the WHIP or WHEP specs. It comes down to getting a handful of WebRTC details right:
a=ssrc
One thing worth knowing as a user, not a builder: WHIP freezes the m= sections after the first POST, so you never renegotiate. A changed stream is a new session by design.
My browser talking to my own server proves nothing. Both sides are mine, so of course they agree. The real test is something I did not write, and that something was OBS.
OBS speaks WHIP natively. You point it at the WHIP URL and it publishes. In theory, my player should then just see it. In practice, a real encoder made me honest about two shortcuts I had taken.
Codec. OBS encodes H.264 by default, not the VP8 my browser used, and my relay cannot transcode. So the relay now reads whatever codec the live publisher is sending and quietly steers each viewer to negotiate the same one. A browser publisher stays on VP8; OBS gets H.264; the player matches whoever is live.
Shape. OBS sends audio and one video, in its own m-line order, with its own identifiers, and no screen. My relay had assumed my own fixed layout. So it stopped trusting position and started matching media by kind and order instead. OBS’s single video lands on the player’s camera slot, and the unused screen slot simply stays dark.
I checked it first with browser stand-ins, a publisher forced to H.264 and a minimal two-track publisher, and both worked. The real proof is pointing OBS itself at the server: OBS set to WHIP with an H.264 encoder, and its scene showing up in a browser that has never heard of OBS. That is the standard earning its name.
A while back I wrote about talking to an AI agent over WebTransport, my first step into Media over QUIC. I loved building it, but I had to invent the signaling by hand: my own little protocol to get the two sides to agree on anything. Looking back with WHIP and WHEP in mind, that is exactly the piece I would replace. The transport is allowed to be the exciting new thing; the handshake does not have to be.
And these two are quietly becoming the default answer for that handshake. WHIP is already an RFC, WHEP is a revision or two behind it, and the tools are lining up: OBS ships WHIP output, Cloudflare’s realtime stack speaks both, and media servers like MediaMTX and Broadcast Box accept them out of the box. Instead of every project bringing its own signaling, WHIP and WHEP open the door to a more standardized handshake that anyone can use, arrived at not by a big migration but by everyone slowly agreeing on the same two HTTP calls.
What I keep thinking about is where that leads. There is a whole ecosystem that has been waiting for exactly this: drones, doorbell cameras, sensors on a machine, anything that can speak WebRTC and make an HTTP request. Connecting them used to mean dragging along SIP, XMPP, or yet another invented protocol just to set up the media. With WHIP, getting a live stream into a server is just a POST.
I am not there yet, and honestly that was never the point. I built this to understand how the pieces fit together, and the shape that came out feels solid: small, leaning on standardization instead of reinventing signaling, and pointed in the right direction. From here I get to look at WHIP and WHEP with the right angle, as real building blocks and not just two odd little names. Although, let’s be honest, they will always sound a bit like a cartoon getting hit with a frying pan, and I can live with that. 🙂
