Getting Started

Onwardz lets you embed real-time 1:1 and small-group video into any web app. Your server creates rooms and issues short-lived tokens; the browser uses those tokens to join — your API key never touches the browser.

Prerequisites

Installation

Install both packages — one for your server, one for the browser:

npm install @onwardz/node   # server-side only
npm install @onwardz/js     # browser-side only

Your first room

1. Create a room and issue a token (server-side)

Use @onwardz/node on your backend. The API key stays on the server.

import { OnwardzClient } from '@onwardz/node';

const onwardz = new OnwardzClient({
  apiKey: process.env.ONWARDZ_API_KEY,
});

// Create a room (once per meeting session)
const { room_id } = await onwardz.createRoom({ max_participants: 4 });

// Issue a short-lived token for each participant
const { token } = await onwardz.issueToken(room_id, 3600);

// Send room_id + token to the browser (e.g. via your API response)

2. Embed the room (browser)

Use the <onwardz-room> web component — it works in any framework or plain HTML.

<script type="module">
  import '@onwardz/js/element';
</script>

<onwardz-room
  room-id="{{ room_id }}"
  token="{{ token }}"
  local-name="{{ current_user.name }}"
></onwardz-room>
Security note: Never pass your API key to the browser. Always generate a token server-side and pass that instead.

Try it live

This is the real <onwardz-room> component — the same drop-in you embed — wired to a throwaway demo room. Click start, allow camera access, then pin a tile or share your screen.

@onwardz/node

Server-side SDK for managing rooms, issuing tokens, and configuring your Onwardz integration. Requires Node.js 18+ (uses native fetch). View on npm →

OnwardzClient

new OnwardzClient({ apiKey, serverUrl? })
OptionTypeDescription
apiKeystringrequiredYour API key from the dashboard (onwardz_...)
serverUrlstringoptionalOverride for self-hosted deployments. Defaults to https://api.onwardz.com/rpc
import { OnwardzClient } from '@onwardz/node';

const onwardz = new OnwardzClient({
  apiKey: process.env.ONWARDZ_API_KEY,
});

createRoom(opts?)

Create a new room. Returns the room ID along with an initial peer ID (for the creating server process, if it needs to join) and whether branding should be shown.

OptionTypeDescription
max_participantsnumberCap on simultaneous participants. Omit for no limit.
expires_atstringISO 8601 expiry (e.g. "2026-12-31T23:59:59Z"). Omit for no expiry.
const { room_id, peer_id, show_branding } =
  await onwardz.createRoom({
    max_participants: 4,
    expires_at: '2026-12-31T23:59:59Z',
  });

issueToken(roomId, ttlSeconds?)

Issue a signed JWT that allows a browser client to join roomId without the API key. Tokens expire after ttlSeconds (default: 3600). Generate one per participant per session.

const { token } = await onwardz.issueToken(room_id);         // 1 hour
const { token } = await onwardz.issueToken(room_id, 300);      // 5 minutes

listRooms()

List all active rooms for this account.

const { rooms } = await onwardz.listRooms();
// rooms: [{ id, max_participants, expires_at, metadata, allowed_origins, created_at }]

configureRoom(roomId, opts)

Update room settings. Only fields you pass are changed; omit a field to leave it as-is. Pass null to clear a field.

OptionTypeDescription
max_participantsnumber | nullParticipant cap. null removes it.
expires_atstring | nullISO 8601 expiry. null removes it.
metadataobject | nullArbitrary JSON attached to the room.
allowed_originsstring[] | nullBrowser Origin allowlist, enforced on join: browser requests whose Origin isn't listed are rejected (-32003). Non-browser joins (no Origin) are unaffected. null allows any origin.
await onwardz.configureRoom(room_id, {
  max_participants: 6,
  metadata: { label: 'team standup' },
});

// Clear the participant cap:
await onwardz.configureRoom(room_id, { max_participants: null });

deleteRoom(roomId)

Soft-delete a room. Active participants are not forcibly disconnected but no new peers can join.

await onwardz.deleteRoom(room_id);

@onwardz/js

Browser SDK for joining Onwardz rooms. Ships a drop-in web component and a programmatic class. View on npm →

<onwardz-room> web component

The simplest integration — a standard web component that renders a full video grid, media controls, screen sharing, and a chat sidebar. Works in any framework or plain HTML.

<script type="module">
  import '@onwardz/js/element';
</script>

<onwardz-room
  room-id="abc-123"
  token="eyJ..."
  local-name="Alex"
  server-url="https://api.onwardz.com/rpc"
></onwardz-room>
AttributeTypeDescription
room-idstringrequiredRoom ID to join
tokenstringrequiredJWT token issued server-side via issueToken()
local-namestringoptionalDisplay name shown to other participants
server-urlstringoptionalOverride for self-hosted deployments. Defaults to https://api.onwardz.com/rpc
show-feedbackbooleanoptionalShow a post-call feedback overlay when the user hangs up. onwardz-hangup fires after the overlay is dismissed.

The element dispatches one DOM event:

EventDetailDescription
onwardz-hangupThe local user clicked "End call"

Sizing

The component fills the viewport by default (height: 100dvh), like a full-screen call — no parent height or extra CSS required.

To embed it in a fixed-size box, set the --onwardz-room-height (and optionally --onwardz-room-width) custom properties:

<style>
  onwardz-room {
    --onwardz-room-height: 600px;
    --onwardz-room-width: 900px;
  }
</style>
Note: a plain height rule on the onwardz-room selector is ignored — the element's shadow :host style wins on specificity. Use the custom properties above to resize it.

OnwardzRoom class

For programmatic control — build your own UI on top of the WebRTC mesh.

new OnwardzRoom({ roomId, token, serverUrl?, localName?, localStream?, iceServers? })
OptionTypeDescription
roomIdstringrequiredRoom ID to join
tokenstringrequiredJWT token from issueToken()
serverUrlstringoptionalDefaults to https://api.onwardz.com/rpc
localNamestringoptionalDisplay name announced to peers on connect
localStreamMediaStreamoptionalLocal camera/mic stream to share
iceServersobject[]optionalCustom STUN/TURN configuration

Methods

MethodReturnsDescription
join()Promise<{ peerId, peers }>Join the room and start the WebRTC mesh
send(text)voidBroadcast a text message to all connected peers
startScreenShare(stream)Promise<void>Share a screen stream with all peers
stopScreenShare()voidStop screen sharing
leave()Promise<void>Leave the room and clean up
on(event, handler)thisRegister an event listener (chainable)

Properties

PropertyTypeDescription
peerIdstring | nullThis peer's ID — available after join()
roomPeersstring[]Peer IDs acknowledged by the server; updated as peers join/leave
connectedPeersstring[]Peer IDs with an open WebRTC data channel (lags slightly behind roomPeers)
showBrandingbooleantrue on free-tier accounts — show the Onwardz branding badge

Events

Listen via room.on(event, handler) or room.addEventListener(event, handler).

EventDetailDescription
peer:joined{ peerId }A peer connected to the room
peer:left{ peerId }A peer disconnected
peer:name{ peerId, name }A peer announced their display name
message{ from, text }A text message was received
peer:track{ peerId, stream }A remote camera/mic stream arrived
peer:screen-track{ peerId, stream }A remote screen share started
peer:screen-stop{ peerId }A remote peer stopped screen sharing

Working with media

import { OnwardzRoom } from '@onwardz/js';

// Acquire camera + mic before joining
const localStream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true,
});

const room = new OnwardzRoom({
  roomId: 'abc-123',
  token: 'eyJ...',
  localStream,
  localName: 'Alex',
});

room.on('peer:track', ({ peerId, stream }) => {
  const video = document.createElement('video');
  video.srcObject = stream;
  video.autoplay = true;
  document.body.appendChild(video);
});

room.on('message', ({ from, text }) => {
  console.log(from, ':', text);
});

const { peerId, peers } = await room.join();

// Send a message
room.send('hello!');

// Share screen
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
await room.startScreenShare(screenStream);

// Leave
await room.leave();

Handle errors by name with the exported RpcErrors constants:

import { OnwardzRoom, RpcErrors } from '@onwardz/js';

try {
  await room.join();
} catch (err) {
  if (err.code === RpcErrors.ROOM_FULL)    alert('Room is full');
  if (err.code === RpcErrors.ROOM_EXPIRED) alert('This room has expired');
  if (err.code === RpcErrors.FORBIDDEN)    alert('Invalid or expired token');
}

API Reference

The Onwardz signaling API is a JSON-RPC 2.0 endpoint. This section documents the raw protocol — use this if you're building a custom client or integrating from a non-JavaScript environment.

JSON-RPC overview

All requests are POST to https://api.onwardz.com/rpc with a JSON body:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "rooms.join",
  "params": { "room_id": "abc-123" }
}

Authenticated methods require an Authorization: Bearer <api_key> header. Methods marked token accept either an API key header or a token param in the body.

Methods

rooms.create

Create a new room. Auth: API key required.

ParamTypeDescription
max_participantsnumberOptional participant cap
expires_atstringOptional ISO 8601 expiry

Returns { room_id, peer_id, show_branding }

rooms.join

Join an existing room. Auth: API key or JWT token.

ParamTypeDescription
room_idstringRoom to join
tokenstringJWT token (alternative to API key auth)

Returns { peer_id, peers: [peer_id, ...], show_branding }peers is the list of peer IDs already in the room.

rooms.signal

Send a WebRTC signaling message (SDP or ICE candidate) to another peer. Auth: none.

ParamTypeDescription
room_idstring
fromstringSender peer ID
tostringRecipient peer ID
payloadobjectArbitrary signaling payload (SDP offer/answer, ICE candidate)

Returns {}

rooms.poll

Long-poll for incoming signals. The server holds the connection open until a message arrives or 30 seconds elapse. Auth: none.

ParamTypeDescription
room_idstring
peer_idstring

Returns { messages: [...] }

rooms.leave

Remove a peer from the room. Auth: none.

ParamTypeDescription
room_idstring
peer_idstring

Returns {}

rooms.issue_token

Issue a signed JWT for room_id. Auth: API key required.

ParamTypeDescription
room_idstring
ttl_secondsnumberToken lifetime. Default: 3600.

Returns { token }

rooms.configure

Update room settings. Auth: API key required.

ParamTypeDescription
room_idstring
max_participantsnumber | null
expires_atstring | null
metadataobject | null
allowed_originsstring[] | nullBrowser Origin allowlist, enforced on join (non-browser joins unaffected).

Returns {}

rooms.list

List all rooms for this account. Auth: API key required.

Returns { rooms: [{ id, max_participants, expires_at, metadata, allowed_origins, created_at }] }

rooms.delete

Soft-delete a room. Auth: API key required.

ParamTypeDescription
room_idstring

Returns {}

Error codes

Standard JSON-RPC codes apply (-32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error). App-specific codes:

ConstantCodeMeaning
ROOM_NOT_FOUND-32000Room does not exist or was deleted
PEER_NOT_FOUND-32001Peer ID not in room's signaling state
FORBIDDEN-32003Invalid or missing API key / token
ROOM_FULL-32004Room has reached its participant cap
ROOM_EXPIRED-32005Room's expiry timestamp has passed
CAP_REACHED-32006Account monthly participant-minute quota exhausted

Rate limits & quotas

Limits keep the platform healthy; well-behaved clients (including the SDKs) won't normally hit them.

LimitBehavior
Per-IP rate limit on /rpcExcess requests receive HTTP 429. Back off and retry — the browser SDK's poll loop does this automatically.
Dashboard login (OTP)Tight per-IP limit on requesting/verifying codes; HTTP 429 when exceeded.
Signal payload sizerooms.signal payloads over 64 KB are rejected (-32602). SDP/ICE messages are far smaller.
Participants per room2–1000 via max_participants; joining a full room returns ROOM_FULL (-32004).
Monthly participant-minutesIncluded: Free 10,000 · Starter 50,000 · Pro 300,000 · Enterprise unlimited. The free tier is hard-capped — exhausting it returns CAP_REACHED (-32006); paid tiers bill metered overage beyond the included minutes (no hard cap).

Rooms can also restrict which browser origins may join — see allowed_origins.