# Quickstart

> From an API key to an object published and received through Relay, in five steps.

URL: https://www.caprid.com/docs/getting-started/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

Sign in to the dashboard at [app.caprid.com/app](https://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:

```bash
read -rs -p 'Switchboard API key: ' SB_API_KEY && export SB_API_KEY
curl 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](https://www.caprid.com/docs/guides/credentials/#api-keys).

## 2. Create a Signing key

Client tokens are signed with one of your Namespace's Signing keys. Create one:

```bash
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](https://www.caprid.com/docs/guides/credentials/#signing-keys) and
[`createSigningKey`](https://www.caprid.com/docs/reference/http/account/operations/createsigningkey/).

## 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:

```bash
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`](https://www.caprid.com/docs/reference/http/account/operations/mintclienttoken/), and check a token with
[`verifyClientToken`](https://www.caprid.com/docs/reference/http/account/operations/verifyclienttoken/).

## 4. Publish and receive one object

The first round trip runs the example in the
[SDK repository](https://github.com/caprids/relay-sdks). You need Git and
[mise](https://mise.jdx.dev/getting-started.html). 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:

**TypeScript (Node)**

```bash
git clone https://github.com/caprids/relay-sdks.git && cd relay-sdks
mise install && mise run ts:install

mise run ts:example:mux-global -- \
  --room acme/quickstart \
  --namespace "run/$(date +%s)" --track ticker --count 1 --timeout 30
```

The task builds the native QUIC addon for Node from source, which needs Rust. mise brings the pinned
toolchain. Installing from npm needs no Rust.

**Python**

```bash
git clone https://github.com/caprids/relay-sdks.git && cd relay-sdks
mise install && mise run python:install

mise run python:example:mux-global -- \
  --room acme/quickstart \
  --namespace "run/$(date +%s)" --track ticker --count 1 --timeout 30
```

The demo publishes one object on track `ticker` and subscribes to it. The run succeeded when it
prints:

```text
finished             published=1 received=1
```

If it doesn't, see "If the run fails" in the [TypeScript](https://www.caprid.com/docs/sdks/typescript/guide/#if-the-run-fails)
or [Python](https://www.caprid.com/docs/sdks/python/guide/#if-the-run-fails) guide.

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

**TypeScript**

Install the SDK and its native QUIC transport for Node 24 or later:

```bash
npm install @caprid/relay @caprid/relay-quic
```

```ts

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 demand
```

In a browser, import from `@caprid/relay` instead, which connects over WebTransport and needs no
`@caprid/relay-quic`. See [Install](https://www.caprid.com/docs/sdks/typescript/#install).

**Python**

Install the SDK:

```bash
uv add caprid-relay
```

```python

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](https://www.caprid.com/docs/sdks/python/guide/#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](https://www.caprid.com/docs/sdks/typescript/guide/#stay-connected-supervised-connections),
[Python](https://www.caprid.com/docs/sdks/python/guide/#stay-connected-supervised-connections)).

## 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](https://www.caprid.com/docs/guides/credentials/#sign-your-own).
- **Give each Client only what it needs.** A viewer's token should only `subscribe`, and only under
  its room: [Client tokens](https://www.caprid.com/docs/guides/credentials/#client-tokens).
- **Keep staging apart from prod.** [Prod and staging](https://www.caprid.com/docs/guides/prod-and-staging/).
- **Handle failures.** Errors have stable codes and phases:
  [TypeScript](https://www.caprid.com/docs/sdks/typescript/guide/#errors-and-recovery),
  [Python](https://www.caprid.com/docs/sdks/python/guide/#errors-and-recovery).
