Skip to main content

Getting Started

Five minutes from pip install to two peers talking.

Install

pip install "metered-realtime[webrtc]"   # MeteredPeer + media
# or, pub/sub only (no WebRTC stack):
pip install metered-realtime

Requires Python 3.10+. The webrtc extra pulls in aiortc + av (a native build); the pub/sub-only install needs just websockets.

Get a key from the dashboard

  1. Sign up (skip if you already have an account).
  2. Dashboard → Realtime Messaging → Keys → Create key.
  3. Pick a key type:
Key typeWhen to useWhere it goes
pk_live_… publishablePrototypes, trusted server-side processes, no per-user scopingDirectly in your code / env
Secret key (sk_id_… + sk_secret_…)Per-user permissions, custom peer_id, embedded TURN credsServer-side only — your backend mints JWTs

For the rest of this page we use the pk_live_ path. See Authentication when you're ready for JWT minting (and to give a peer Send permission, which publishable keys don't grant by default).

Your first connection — pub/sub

SignallingClient is the smaller surface: pub/sub, no WebRTC. Good for telemetry, coordination, anywhere you don't need peer-to-peer media.

import asyncio
from metered_realtime import SignallingClient, Connected, Message

async def main() -> None:
async with SignallingClient(api_key="pk_live_…") as client:
@client.on(Connected)
def _(ev: Connected) -> None:
print("connected as", ev.peer_id)

@client.on(Message)
def _(ev: Message) -> None:
print(f"{ev.sender_peer_id}{ev.channel}:", ev.data)

await client.subscribe("room-42")
await client.publish("room-42", {"hello": "world"})
await asyncio.sleep(5)

asyncio.run(main())

Run two copies side by side. Each gets a different peer_id; one's publish shows up as a Message event in the other. That's the whole pub/sub model — subscribe to channels, publish to channels, receive Message events.

Add peers (WebRTC)

MeteredPeer is the same connection plus channel-driven peer discovery, a per-peer connection lifecycle, and media fan-out. Use it when peers exchange audio/video or low-latency P2P data.

import asyncio
from metered_realtime import MeteredPeer, PeerJoined, Track, Data

async def main() -> None:
async with MeteredPeer(api_key="pk_live_…") as peer:
@peer.on(PeerJoined)
def _(ev: PeerJoined) -> None:
print("someone joined:", ev.peer.id)

@ev.peer.on(Track)
def _(t: Track) -> None:
# t.track is an aiortc MediaStreamTrack — record it, transcribe it, …
print("receiving", t.track.kind)

@peer.on(Data)
def _(ev: Data) -> None:
print(ev.sender_peer_id, "said:", ev.data)

await peer.join("my-room")
await peer.send({"hi": "everyone"})
await asyncio.sleep(30)

asyncio.run(main())

What this does:

  1. join("my-room") connects, subscribes you to my-room, and asks the server who else is here.
  2. For every peer the server reports, PeerJoined fires and the SDK opens a peer-to-peer connection under the hood.
  3. You receive each peer's media on the Track event; anything you attached with add_track is sent to every peer.
  4. peer.send(data) broadcasts to everyone in the channel; peer.send_to(remote_id, data) targets one peer.

Peers don't arrive synchronously with join. The server's first presence for the channel lands shortly after the subscribe ack, so peer.remote_peers is typically still empty right after await peer.join(...). Populate your state from the PeerJoined handler, not a snapshot.

Sending media

There's no getUserMedia on the server, so you supply the track. For an AI agent streaming synthesized speech:

from metered_realtime import AudioSource, MediaStream

source = AudioSource(input_rate=16_000) # 16 kHz mono PCM in
peer.add_track(source, MediaStream(id="agent-voice"))
await source.push(pcm_bytes) # emitted as real-time 48 kHz audio
source.end()

Or feed from a file / IP camera (.audio / .video is None if the source lacks it):

from metered_realtime import from_file, from_rtsp

peer.add_track(from_file("clip.mp4").audio)
peer.add_track(from_rtsp("rtsp://cam:554/stream").video)

See the Media reference for AudioSource, iter_frames (consuming a peer's audio for STT), and all the source helpers.

Messages are server-routed by default

peer.send(data) / peer.send_to(id, data) are server-routed, not P2P over a data channel. They work before ICE completes, but go Peer → Metered server → Peer, so each message counts against your signalling quota. For low-latency P2P data, open a real data channel:

from metered_realtime import DataChannel, DCMessage

raw = ev.peer.create_data_channel("game-state")
dc = DataChannel(raw)

@dc.on(DCMessage)
def _(m: DCMessage) -> None:
handle(m.data)

See Data Channels for the backpressure-aware wrapper and the reconnect gotcha.

Next step depends on what you're building

You're building…Read
An AI voice agentGuide: AI Agent Communication
An IoT / edge camera bridgeGuide: IoT Telemetry
Anything going to productionGuide: Reconnect Best Practices — required reading
Per-user JWTs (custom peer_id, channel permissions, TURN creds)Guide: Authentication