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
- Node.js 18+ for the server SDK
- An Onwardz account and API key — get one from the dashboard
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>
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
| Option | Type | Description | |
|---|---|---|---|
apiKey | string | required | Your API key from the dashboard (onwardz_...) |
serverUrl | string | optional | Override 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.
| Option | Type | Description |
|---|---|---|
max_participants | number | Cap on simultaneous participants. Omit for no limit. |
expires_at | string | ISO 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.
| Option | Type | Description |
|---|---|---|
max_participants | number | null | Participant cap. null removes it. |
expires_at | string | null | ISO 8601 expiry. null removes it. |
metadata | object | null | Arbitrary JSON attached to the room. |
allowed_origins | string[] | null | Browser 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>
| Attribute | Type | Description | |
|---|---|---|---|
room-id | string | required | Room ID to join |
token | string | required | JWT token issued server-side via issueToken() |
local-name | string | optional | Display name shown to other participants |
server-url | string | optional | Override for self-hosted deployments. Defaults to https://api.onwardz.com/rpc |
show-feedback | boolean | optional | Show a post-call feedback overlay when the user hangs up. onwardz-hangup fires after the overlay is dismissed. |
The element dispatches one DOM event:
| Event | Detail | Description |
|---|---|---|
onwardz-hangup | — | The 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>
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.
| Option | Type | Description | |
|---|---|---|---|
roomId | string | required | Room ID to join |
token | string | required | JWT token from issueToken() |
serverUrl | string | optional | Defaults to https://api.onwardz.com/rpc |
localName | string | optional | Display name announced to peers on connect |
localStream | MediaStream | optional | Local camera/mic stream to share |
iceServers | object[] | optional | Custom STUN/TURN configuration |
Methods
| Method | Returns | Description |
|---|---|---|
join() | Promise<{ peerId, peers }> | Join the room and start the WebRTC mesh |
send(text) | void | Broadcast a text message to all connected peers |
startScreenShare(stream) | Promise<void> | Share a screen stream with all peers |
stopScreenShare() | void | Stop screen sharing |
leave() | Promise<void> | Leave the room and clean up |
on(event, handler) | this | Register an event listener (chainable) |
Properties
| Property | Type | Description |
|---|---|---|
peerId | string | null | This peer's ID — available after join() |
roomPeers | string[] | Peer IDs acknowledged by the server; updated as peers join/leave |
connectedPeers | string[] | Peer IDs with an open WebRTC data channel (lags slightly behind roomPeers) |
showBranding | boolean | true on free-tier accounts — show the Onwardz branding badge |
Events
Listen via room.on(event, handler) or room.addEventListener(event, handler).
| Event | Detail | Description |
|---|---|---|
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.
| Param | Type | Description |
|---|---|---|
max_participants | number | Optional participant cap |
expires_at | string | Optional ISO 8601 expiry |
Returns { room_id, peer_id, show_branding }
rooms.join
Join an existing room. Auth: API key or JWT token.
| Param | Type | Description |
|---|---|---|
room_id | string | Room to join |
token | string | JWT 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.
| Param | Type | Description |
|---|---|---|
room_id | string | |
from | string | Sender peer ID |
to | string | Recipient peer ID |
payload | object | Arbitrary 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.
| Param | Type | Description |
|---|---|---|
room_id | string | |
peer_id | string |
Returns { messages: [...] }
rooms.leave
Remove a peer from the room. Auth: none.
| Param | Type | Description |
|---|---|---|
room_id | string | |
peer_id | string |
Returns {}
rooms.issue_token
Issue a signed JWT for room_id. Auth: API key required.
| Param | Type | Description |
|---|---|---|
room_id | string | |
ttl_seconds | number | Token lifetime. Default: 3600. |
Returns { token }
rooms.configure
Update room settings. Auth: API key required.
| Param | Type | Description |
|---|---|---|
room_id | string | |
max_participants | number | null | |
expires_at | string | null | |
metadata | object | null | |
allowed_origins | string[] | null | Browser 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.
| Param | Type | Description |
|---|---|---|
room_id | string |
Returns {}
Error codes
Standard JSON-RPC codes apply (-32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error). App-specific codes:
| Constant | Code | Meaning |
|---|---|---|
ROOM_NOT_FOUND | -32000 | Room does not exist or was deleted |
PEER_NOT_FOUND | -32001 | Peer ID not in room's signaling state |
FORBIDDEN | -32003 | Invalid or missing API key / token |
ROOM_FULL | -32004 | Room has reached its participant cap |
ROOM_EXPIRED | -32005 | Room's expiry timestamp has passed |
CAP_REACHED | -32006 | Account monthly participant-minute quota exhausted |
Rate limits & quotas
Limits keep the platform healthy; well-behaved clients (including the SDKs) won't normally hit them.
| Limit | Behavior |
|---|---|
Per-IP rate limit on /rpc | Excess 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 size | rooms.signal payloads over 64 KB are rejected (-32602). SDP/ICE messages are far smaller. |
| Participants per room | 2–1000 via max_participants; joining a full room returns ROOM_FULL (-32004). |
| Monthly participant-minutes | Included: 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.