URL: /docs/sdk-server

---
title: "@rill/server"
description: "Node SDK: create calls, mint tokens, verify webhooks."
icon: server
---

{/* Generated by docs/scripts/generate-sdk-docs.mjs via TypeDoc from sdk/server/src/index.ts. Do not edit. */}

Mint tokens on your **server**. The API secret never goes to the browser. Walkthrough: [Your app](/your-app).

```sh
npm i @rill/server
```

Until a `v*` tag exists, use this repo's `sdk/` workspace.

```ts
import { Rill } from "@rill/server";

const rill = new Rill({
  url: "https://rill.example.com",
  apiKey: process.env.RILL_API_KEY!,
  apiSecret: process.env.RILL_API_SECRET!,
});

const call = await rill.calls.create();
const token = call.token({
  id: "alice",
  name: "Alice",
  publish: { microphone: true, camera: true, screen: false, data: true },
  ttl: "15m",
});
console.log(call.id, token);
```

## Create a call

```ts
new Rill(opts: RillOptions)
rill.calls.create(opts?: { recording?: boolean }): Promise<Call>
call.id: string
call.token(opts: TokenOptions): string
rill.calls.token(callId: string, opts: TokenOptions): string
call.end(): Promise<void>

type RillOptions = {
  url: string; // SFU origin, or operator Caddy that reverse-proxies `/v1`. Not the try-it site (`:3000...
  apiKey: string;
  apiSecret: string; // HS256 secret. Join tokens and join/leave webhook HMAC. Never send to the browser.
  fetch?: typeof fetch; // Injected in tests. Defaults to global `fetch`.
};

type TokenOptions = {
  id: string; // Opaque participant id (`sub`).
  name?: string; // Optional display name (`nam` on the wire).
  publish: Publish;
  ttl?: string | number; // Duration string (`15m`, `1h`) or seconds. Default `15m`. Max 24h.
};

type Publish = {
  microphone: boolean;
  camera: boolean;
  screen: boolean;
  data: boolean;
};
```

- **`new Rill`** — `url`, `apiKey`, `apiSecret`. Never construct this in the browser.
- **`rill.calls.create`** — `POST /v1/calls`. `{ recording: true }` asks the sidecar to arm; sidecar down → `recording: false` and 201 still.
- **`call.id`** — Persist this; mint later with `rill.calls.token(id, opts)`. No `url` on the JSON.
- **`call.token`** — Local HS256 for this call id. Same as `rill.calls.token(this.id, opts)`.
- **`rill.calls.token`** — Local HS256 JWT. Does not call the SFU. `publish` requires all four keys. Default TTL 15m, max 24h.
- **`call.end`** — Destroy this call.
- **`Publish`** — All four keys required. `data` gates send; receive always works.

## Look up, kick, mute

```ts
rill.calls.get(id: string): Promise<Call>
rill.calls.list(): Promise<Call[]>
rill.calls.participants(id: string): Promise<Participant[]>
rill.calls.kick(id: string, participantId: string): Promise<void>
call.unpublish(opts: { participantId: string; media: MediaKind }): Promise<void>
```

- **`rill.calls.get`** — `GET /v1/calls/:id`.
- **`rill.calls.list`** — `GET /v1/calls`. In-memory list on this process.
- **`rill.calls.participants`** — `GET .../participants`.
- **`rill.calls.kick`** — `DELETE .../participants/:id`. Emits `participant.left`.
- **`call.unpublish`** — Force-disable one media kind on a participant (`POST .../unpublish`).

## Recording

```ts
call.recording: boolean
call.recordingStart(): Promise<RecordingStart>
call.recordingStop(): Promise<RecordingStop>
```

- **`call.recording`** — Sidecar armed for this call. False if recording was requested but the sidecar was down.
- **`call.recordingStart`** — `POST .../recording/start`. Sidecar down → `recording_unavailable`. The live call does not wait on S3.
- **`call.recordingStop`** — `POST .../recording/stop`. Mux + upload happen on the sidecar; then `recording.stopped`.

## Webhooks

```ts
verifyWebhook(opts: VerifyWebhookOpts): VerifyWebhookResult

type VerifyWebhookOpts = {
  secret: string; // `RILL_API_SECRET` (join/leave) or `RILL_RECORD_WEBHOOK_SECRET` (recording.stopped).
  timestamp: string; // `X-Rill-Timestamp` (unix seconds).
  signature: string; // `X-Rill-Signature` (hex HMAC-SHA256 of `timestamp.body`).
  body: string | Buffer | Uint8Array; // Raw POST body. Do not `JSON.parse` first.
  maxSkewSeconds?: number; // Max |now - timestamp| in seconds. Default 300.
  nowSeconds?: number; // Injected clock for tests.
};

type VerifyWebhookResult = { ok: true } | { ok: false; code: "unauthorized" | "timestamp" };
```

- **`verifyWebhook`** — Verify Rill webhook HMAC over the raw POST body. After `ok`, `JSON.parse` and persist `id` (treat duplicate `id` as idempotent).

## Errors

```ts
type ErrorCode =
  | "unauthorized"
  | "forbidden"
  | "call_not_found"
  | "call_full"
  | "call_ended"
  | "participant_invalid"
  | "token_expired"
  | "overloaded"
  | "server_shutdown"
  | "unsupported"
  | "recording_unavailable"
  | "recording_active"
  | "permission_denied"
  | "connection_failed"
  | "internal";
```

- **`ErrorCode`** — Public error `code` strings. Match the Rill HTTP JSON `{ error: { code, message } }`.
