Quickstart
This takes you from a new Organization to one object published and received through a relay. You’ll
use curl and jq to talk to Switchboard, and the SDK repository’s example to talk to the relay.
The examples use the Namespace acme. Use yours wherever you see it.
1. Get an API key
Section titled “1. Get an API key”Sign in to the dashboard at app.caprid.com/app. Signing up creates an Organization with the Namespace you choose, and shows its first API key once. Keep it in your shell, and in your secret manager after that:
read -rs -p 'Switchboard API key: ' SB_API_KEY && export SB_API_KEYcurl https://app.caprid.com/v1/account -H "Authorization: Bearer $SB_API_KEY"The API key manages your Organization. Relays never accept it, but it can mint tokens that they do, so treat it like a password and keep it on your servers. See API keys.
2. Create a Signing key
Section titled “2. Create a Signing key”Client tokens are signed with one of your Namespace’s Signing keys. Create one:
curl -X POST https://app.caprid.com/v1/namespaces/acme/keys \ -H "Authorization: Bearer $SB_API_KEY" \ -d '{"label": "backend"}'The response is a JWK with kid acme_backend. Its k is the secret your backend will sign
tokens with in production; for now, Switchboard can sign them for you.
See Signing keys and
createSigningKey.
3. Mint a Client token
Section titled “3. Mint a Client token”Ask for a token rooted at acme/quickstart that may both publish and subscribe beneath it. An empty
prefix ("") means everything under the root:
export RELAY_TOKEN=$(curl -s -X POST https://app.caprid.com/v1/namespaces/acme/tokens \ -H "Authorization: Bearer $SB_API_KEY" \ -d '{"path": "quickstart", "publish": "", "subscribe": ""}' | jq -r .token)Paths you send to the API are relative to the Namespace, so the token’s root claim is
acme/quickstart. It expires in an hour. See
mintClientToken, and check a token with
verifyClientToken.
4. Publish and receive one object
Section titled “4. Publish and receive one object”The first round trip runs the example in the
SDK repository. You need Git and
mise. The example reads RELAY_TOKEN from the
environment, so it doesn’t prompt.
--room is your token’s root. The example connects to https://relay.mux.global/acme/quickstart,
and the namespace you pass is relative to that root. Pick a fresh namespace for every run:
git clone https://github.com/caprids/relay-sdks.git && cd relay-sdksmise install && mise run ts:install
mise run ts:example:mux-global -- \ --room acme/quickstart \ --namespace "run/$(date +%s)" --track ticker --count 1 --timeout 30The task builds the native QUIC addon for Node from source, which needs Rust. mise brings the pinned toolchain. Installing from npm needs no Rust.
git clone https://github.com/caprids/relay-sdks.git && cd relay-sdksmise install && mise run python:install
mise run python:example:mux-global -- \ --room acme/quickstart \ --namespace "run/$(date +%s)" --track ticker --count 1 --timeout 30The demo publishes one object on track ticker and subscribes to it. The run succeeded when it
prints:
finished published=1 received=1If it doesn’t, see “If the run fails” in the TypeScript or Python guide.
5. Do it from your own code
Section titled “5. Do it from your own code”The same round trip, with the SDK in your own project. roomEndpoint puts the token’s root in the
URL of the hosted Relay, so the connection can’t reach outside it, and the namespace is relative to
that root.
Install the SDK and its native QUIC transport for Node 24 or later:
npm install @caprid/relay @caprid/relay-quicimport { connectRelayPublisher, connectRelaySubscriber, readText, roomEndpoint } from '@caprid/relay/node';
const url = roomEndpoint(['acme', 'quickstart']);const namespace = ['run', String(Date.now())];const getToken = () => process.env.RELAY_TOKEN;
const publisher = await connectRelayPublisher({ url, namespace, getToken });const ticker = publisher.registerTrack({ track: 'ticker' });await publisher.announced;
const subscriber = await connectRelaySubscriber({ url, getToken });const subscription = subscriber.subscribe( { namespace, track: 'ticker' }, { onObject: (object) => { if (object.status === 'normal') console.log(readText(object)); } },);await subscription.subscribed; // a fresh namespace can briefly reject this with error 16; retry it
await ticker.writeText('hello from Node'); // waits for the subscriber's demandIn a browser, import from @caprid/relay instead, which connects over WebTransport and needs no
@caprid/relay-quic. See Install.
Install the SDK:
uv add caprid-relayimport asyncio, os, time
from caprid_relay import ( MoqtObject, RelaySubscriptionHandlers, connect_relay_publisher, connect_relay_subscriber, read_text, room_endpoint,)
async def main() -> None: token = os.environ["RELAY_TOKEN"] url = room_endpoint(["acme", "quickstart"]) namespace = ["run", str(int(time.time()))] arrived: asyncio.Future[str] = asyncio.get_running_loop().create_future()
def on_object(obj: MoqtObject) -> None: if obj.status == "normal" and not arrived.done(): arrived.set_result(read_text(obj))
async with ( await connect_relay_publisher(url, namespace=namespace, get_token=lambda: token) as publisher, await connect_relay_subscriber(url, get_token=lambda: token) as subscriber, ): ticker = publisher.register_track("ticker") await publisher.announced subscription = subscriber.subscribe(namespace, "ticker", RelaySubscriptionHandlers(on_object=on_object)) await subscription.subscribed # a fresh namespace can briefly reject this with error 16; retry it await ticker.write_text("hello from caprid-relay") # waits for the subscriber's demand print(await arrived)
asyncio.run(main())Run it with uv run your_script.py. See
Use the local packages in your application.
These connections never reconnect. For a live application, use the supervised connections, which reconnect with backoff and keep tracks registered and subscribed (TypeScript, Python).
Next steps
Section titled “Next steps”- Sign tokens in your backend. Minting through the API is fine for testing. In production, sign them yourself with the Signing key, which saves a round trip per viewer and keeps working if Switchboard is unreachable: Sign your own.
- Give each Client only what it needs. A viewer’s token should only
subscribe, and only under its room: Client tokens. - Keep staging apart from prod. Prod and staging.
- Handle failures. Errors have stable codes and phases: TypeScript, Python.