Try a call

@rill/server

Node SDK: create calls, mint tokens, verify webhooks.

Mint tokens on your server. The API secret never goes to the browser. Walkthrough: 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 Rillurl, apiKey, apiSecret. Never construct this in the browser.
  • rill.calls.createPOST /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.getGET /v1/calls/:id.
  • rill.calls.listGET /v1/calls. In-memory list on this process.
  • rill.calls.participantsGET .../participants.
  • rill.calls.kickDELETE .../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.recordingStartPOST .../recording/start. Sidecar down → recording_unavailable. The live call does not wait on S3.
  • call.recordingStopPOST .../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 } }.

Type to search…

↑↓ navigate open esc close