diff --git a/examples/whip-whep-data-channels/README.md b/examples/whip-whep-data-channels/README.md
new file mode 100644
index 00000000000..6014b3469bd
--- /dev/null
+++ b/examples/whip-whep-data-channels/README.md
@@ -0,0 +1,43 @@
+# whip-whep
+whip-whep demonstrates using WHIP and WHEP with Pion. Since WHIP+WHEP is standardized signaling you can publish via tools like OBS and GStreamer.
+You can then watch it in sub-second time from your browser, or pull the video back into OBS and GStreamer via WHEP.
+
+Further details about the why and how of WHIP+WHEP are below the instructions.
+
+## Instructions
+
+### Download whip-whep
+
+This example requires you to clone the repo since it is serving static HTML.
+
+```
+git clone https://github.com/pion/webrtc.git
+cd webrtc/examples/whip-whep
+```
+
+### Run whip-whep
+Execute `go run *.go`
+
+### Publish
+
+You can publish via an tool that supports WHIP or via your browser. To publish via your browser open [http://localhost:8080](http://localhost:8080), and press publish.
+
+To publish via OBS set `Service` to `WHIP` and `Server` to `http://localhost:8080/whip`. The `Bearer Token` can be whatever value you like.
+
+
+### Subscribe
+
+Once you have started publishing open [http://localhost:8080](http://localhost:8080) and press the subscribe button. You can now view your video you published via
+OBS or your browser.
+
+Congrats, you have used Pion WebRTC! Now start building something cool
+
+## Why WHIP/WHEP?
+
+WHIP/WHEP mandates that a Offer is uploaded via HTTP. The server responds with a Answer. With this strong API contract WebRTC support can be added to tools like OBS.
+
+For more info on WHIP/WHEP specification, feel free to read some of these great resources:
+- https://webrtchacks.com/webrtc-cracks-the-whip-on-obs/
+- https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
+- https://datatracker.ietf.org/doc/draft-ietf-wish-whep/
+- https://bloggeek.me/whip-whep-webrtc-live-streaming
diff --git a/examples/whip-whep-data-channels/index.html b/examples/whip-whep-data-channels/index.html
new file mode 100644
index 00000000000..148de5bd9b2
--- /dev/null
+++ b/examples/whip-whep-data-channels/index.html
@@ -0,0 +1,97 @@
+
+
+
+
+ whip-whep
+
+
+
+
+
+
+Message
+
+
+
Logs
+
+
+
+
ICE Connection States
+
+
+
+
+
diff --git a/examples/whip-whep-data-channels/main.go b/examples/whip-whep-data-channels/main.go
new file mode 100644
index 00000000000..c33f11a5513
--- /dev/null
+++ b/examples/whip-whep-data-channels/main.go
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2023 The Pion community
+// SPDX-License-Identifier: MIT
+
+//go:build !js
+// +build !js
+
+// whip-whep demonstrates how to use the WHIP/WHEP specifications to exchange SPD descriptions
+// and stream media to a WebRTC client in the browser or OBS.
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/pion/webrtc/v4"
+)
+
+// nolint: gochecknoglobals
+var (
+ peerConnectionConfiguration = webrtc.Configuration{
+ ICEServers: []webrtc.ICEServer{
+ {
+ URLs: []string{"stun:stun.l.google.com:19302"},
+ },
+ },
+ }
+)
+
+// nolint:gocognit
+func main() {
+ // Everything below is the Pion WebRTC API! Thanks for using it ❤️.
+ http.Handle("/", http.FileServer(http.Dir(".")))
+ http.HandleFunc("/whep", whepHandler)
+ http.HandleFunc("/whip", whipHandler)
+
+ fmt.Println("Open http://localhost:8080 to access this demo")
+ panic(http.ListenAndServe(":8080", nil)) // nolint: gosec
+}
+
+func whipHandler(res http.ResponseWriter, req *http.Request) {
+ // Read the offer from HTTP Request
+ offer, err := io.ReadAll(req.Body)
+ if err != nil {
+ panic(err)
+ }
+
+ // Create a new RTCPeerConnection
+ peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfiguration)
+ if err != nil {
+ panic(err)
+ }
+
+ // Send answer via HTTP Response
+ writeAnswer(res, peerConnection, offer, "/whip")
+}
+
+func whepHandler(res http.ResponseWriter, req *http.Request) {
+ // Read the offer from HTTP Request
+ offer, err := io.ReadAll(req.Body)
+ if err != nil {
+ panic(err)
+ }
+
+ // Create a new RTCPeerConnection
+ peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfiguration)
+ if err != nil {
+ panic(err)
+ }
+
+ // Send answer via HTTP Response
+ writeAnswer(res, peerConnection, offer, "/whep")
+}
+
+func writeAnswer(res http.ResponseWriter, peerConnection *webrtc.PeerConnection, offer []byte, path string) {
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+
+ if connectionState == webrtc.ICEConnectionStateFailed {
+ _ = peerConnection.Close()
+ }
+ })
+
+ if err := peerConnection.SetRemoteDescription(webrtc.SessionDescription{
+ Type: webrtc.SDPTypeOffer, SDP: string(offer),
+ }); err != nil {
+ panic(err)
+ }
+
+ // Create channel that is blocked until ICE Gathering is complete
+ gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
+
+ // Create answer
+ answer, err := peerConnection.CreateAnswer(nil)
+ if err != nil {
+ panic(err)
+ } else if err = peerConnection.SetLocalDescription(answer); err != nil {
+ panic(err)
+ }
+
+ // Block until ICE Gathering is complete, disabling trickle ICE
+ // we do this because we only can exchange one signaling message
+ // in a production application you should exchange ICE Candidates via OnICECandidate
+ <-gatherComplete
+
+ // WHIP+WHEP expects a Location header and a HTTP Status Code of 201
+ res.Header().Add("Location", path)
+ res.WriteHeader(http.StatusCreated)
+
+ // Write Answer with Candidates as HTTP Response
+ fmt.Fprint(res, peerConnection.LocalDescription().SDP) //nolint: errcheck
+}