Build with VideoRayan
Everything you need to go from "meeting link" to "embedded product feature". Pair this guide with the API reference for exact schemas.
Getting started
VideoRayan has two integration surfaces. Start with whichever matches your product today — you can switch later without touching client code, because both surfaces produce the same thing: a LiveKit token for wss://rtc.videorayan.ir.
Link-based (5 minutes)
Create a room from curl or the hosted page, share /r/{roomId}, done. Guests enter a display name and join. Best for meetings, consultations, and internal tools.
Owned rooms (an afternoon)
Your backend creates tenant rooms, mints host/guest tokens per user, and controls the lifecycle. Best for products where calls are a feature, not a link.
Backend guide →# create a room and open the hosted UI — that's the whole demo ROOM=$(curl -s -X POST https://api.videorayan.ir/v1/public/rooms \ -H "Content-Type: application/json" -d '{"name":"my-first-room"}' | jq -r .roomId) open "https://en.videorayan.ir/r/$ROOM" # macOS (linux: xdg-open)
Core concepts
Four ideas cover 95% of the platform. Everything else is parameters.
| Concept | What it is | Rules that matter |
|---|---|---|
| Room | The meeting container with a lifecycle (active → ended/expired). |
Free rooms: name is the handle, 12 h default TTL, 50-participant default cap. Tenant rooms: UUID + your externalId, TTL up to 24 h, operator-set cap. |
| Ref | How you address a room: its UUID or, for public rooms, its name. | Works in /v1/public/rooms/{ref}, /v1/guest/rooms/{ref}/tokens, and the hosted UI /r/{ref}. |
| Token | A signed LiveKit JWT scoped to exactly one room with minimal grants. | Default TTL 10 min (max 1 h). Not renewable — mint a new one per session. Treat as a credential: never persist it server-side. |
| Identity | The participant's unique name inside the room. | 1–40 chars on the guest surface, ≤128 on tenant tokens. Duplicates can't coexist in the same room — the newer connection replaces the older one. |
host or guest; guest-surface tokens are always guest. Roles map to LiveKit grants server-side — the client cannot self-upgrade.
Guide: the browser join flow
This is the exact flow the hosted join page implements — replicate it when you build your own join screen.
- Resolve the ref.
GET /v1/public/rooms/{ref}→ learn the canonicalroomId, displayname, and whetherneedsPasswordis true. Show the password field only when needed. - Collect identity + media consent. Ask for a display name; request camera/mic with
getUserMediaso the user can preview before joining. Remember their mic/cam choices and apply them after connect. - Mint the token.
POST /v1/guest/rooms/{roomId}/tokenswith the identity (and password when required). - Handle the outcomes. 201 → connect; 403 → wrong password, re-prompt; 409 → room full/ended; 404 → bad link, send home.
- Connect.
new Room().connect('wss://rtc.videorayan.ir', token)— with one retry on transient failure.
const API = 'https://api.videorayan.ir/v1'; async function joinRoom(ref, identity, password) { // 1 — resolve (also validates the link before asking the user anything) const info = await fetch(`${API}/public/rooms/${encodeURIComponent(ref)}`).then(r => r.json()); if (!info.roomId) throw new Error('room_not_found'); // 2 + 3 — mint the token (password only when the room needs one) const res = await fetch(`${API}/guest/rooms/${info.roomId}/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity, ...(info.needsPassword ? { password } : {}) }) }); // 4 — handle outcomes explicitly if (res.status === 403) throw new Error('wrong_password'); if (res.status === 409) throw new Error('room_full_or_ended'); if (!res.ok) throw new Error('token_failed_' + res.status); const { token } = await res.json(); // 5 — connect (single retry on transient failure) const { Room } = await import('livekit-client'); const room = new Room({ adaptiveStream: false, dynacast: true }); await room.connect('wss://rtc.videorayan.ir', token); return room; }
Guide: backend (tenant) flow
For products that own rooms. The API key lives in your backend environment only — the browser never sees it.
1 — Create the room when your domain event happens
An appointment is booked, a support ticket escalates, a game match starts. Create the room then, with an idempotency key so retries never duplicate rooms:
async function ensureRoom(appointmentId: string) { const res = await fetch('https://api.videorayan.ir/v1/rooms', { method: 'POST', headers: { Authorization: `Bearer ${process.env.VIDEORAYAN_KEY!}`, Content-Type: 'application/json', Idempotency-Key: `apt-${appointmentId}`, // same key → same room on retry }, body: JSON.stringify({ externalId: appointmentId, // your correlation ID maxParticipants: 3, // pilot cap expiresInSeconds: 3600, // 1-hour consultation window }), }); if (!res.ok) throw new Error('room create failed: ' + res.status); return res.json() as Promise<{ roomId: string; status: string }>; }
2 — Mint a token per participant, per session
async function joinAs(roomId: string, user: { id: string; name: string; isDoctor: boolean }) { const res = await fetch(`https://api.videorayan.ir/v1/rooms/${roomId}/tokens`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.VIDEORAYAN_KEY!}`, Content-Type: 'application/json', }, body: JSON.stringify({ identity: user.id, // unique per room name: user.name, role: user.isDoctor ? 'host' : 'guest', ttlSeconds: 600, }), }); return res.json() as Promise<{ token: string; wsUrl: string; expiresAt: string }>; }
3 — Connect from your front-end
// token comes from YOUR backend endpoint, which called mint-token() above import { LiveKitRoom, VideoConference } from '@livekit/components-react'; export function Consultation({ token, wsUrl }: { token: string; wsUrl: string }) { return ( <LiveKitRoom token={token} serverUrl={wsUrl} connect={true} style={{ height: '100vh' }}> <VideoConference /> </LiveKitRoom> ); }
4 — End the room when the appointment is over
DELETE /v1/rooms/{roomId} disconnects everyone, rejects new tokens, and blocks re-join. Status becomes ended (or expired via the sweeper).
Hosted UI & invite links
The English portal ships a production-ready UI you can use instead of building your own — every route also accepts ?room= for static hosting.
| URL | Page | Notes |
|---|---|---|
/r/{roomId} | In-call UI | The canonical invite target. Desktop & mobile in one responsive page. |
/join/{ref} | Join (device check) | Resolves the room, asks for identity/password, then redirects into the call. |
/create | Create room | Free rooms with optional password; produces link + QR + share buttons. |
/?room=… variants | Static-hosting fallback | e.g. /join.html?room={ref} — useful behind static hosts without rewrites. |
https://en.videorayan.ir/r/{roomId} needs zero integration; your own
https://app.yourdomain.com/call/{externalId} can resolve externalId → roomId in your DB and redirect.
In-call features
- Active-speaker stage with click-to-pin; local camera preview with mic/cam toggles carried into the call.
- Screen share from the browser (display/window/tab) — published as a second video track.
- Ephemeral chat with chunked file transfer up to 10 MB over WebRTC data channels. Nothing is stored server-side; closing the room is a true shred.
- Local recording (hosted UI): captures the main stage + mixed audio to a WebM file, plus a chat transcript — generated in the recorder's browser, no server storage.
- Self-healing UI: tracks re-attach automatically after connection hiccups; audio is always preserved first on weak links.
Mobile & desktop SDKs
import LiveKitSDK let room = Room() try await room.connect(url: "wss://rtc.videorayan.ir", token: token) // minted by your backend
val room = Room() room.connect("wss://rtc.videorayan.ir", token) // Flutter is identical: // await room.connect('wss://rtc.videorayan.ir', token);
Security practices
- Keys stay server-side.
rtck_…tenant keys must never appear in front-end code, mobile bundles, or git. Rotate via the operator if leaked. - Least-privilege tokens. Tokens are scoped to one room, one identity, one TTL. Host powers are granted by role at mint time, enforced server-side.
- Passwords are verified, not trusted. Private public-rooms check the password at token issuance — the client can't bypass it.
- Transport is encrypted. DTLS-SRTP for media, WSS for signaling, TLS everywhere else. E2EE is a roadmap item, not a v1 feature — don't claim it to users.
- Anti-enumeration. Cross-tenant access returns 404, indistinguishable from "doesn't exist". Don't build UI that depends on distinguishing them.
- Rate limits are your friend. 120 req/min per key/IP. Cache room resolutions; don't poll.
Production checklist
- Key stored in a secrets manager — never in env files committed to git.
- Idempotency keys used on every room create.
- Token minting endpoint authenticated with your app's auth.
- Rooms ended (
DELETE /rooms/{id}) when your domain event completes. - 403/404/409/429 handled with user-friendly copy (see troubleshooting).
- Monitor
GET /v1/health(liveness) andGET /v1/ready(dependencies).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 429 on room create | Per-IP rate limit hit (shared NAT, load test) | Back off exponentially; for load tests contact the operator for a dedicated tenant key. |
| 403 on guest token | Wrong/missing room password | Resolve the room first and collect the password when needsPassword is true. |
| 409 on token | Room full (cap) or ended/expired | Surface "room is full/ended"; create a new room for a new session. |
| 404 on resolve | Bad link, or room expired (12 h default for free rooms) | Create a fresh room; don't treat 404 as retryable. |
| Token works locally, fails in prod | Token minted for a different room or expired in transit | Mint per join, use immediately; check clock skew if you self-mint. |
| Media connects, no audio from a participant | Participant never granted mic permission, or OS-level mute | Check getUserMedia errors; the platform requests audio on every join. |
FAQ
Do I need an API key to try VideoRayan?
/r/{roomId} link, and guests join with a display name. API keys are only needed for tenant-owned rooms with roles and server-side caps.How long do join tokens live?
How many participants can a room have?
Is media encrypted?
Can I embed VideoRayan in my own product?
/r/{roomId}, or connect directly from your own front-end using any LiveKit SDK with a token you minted server-side. Both use the same rooms.