livqenoDocs

Get startedQuickstart

Quickstart

Create a project, mint a token on your backend, and join a video call from Web, React, React Native or Flutter — in about ten minutes.

In this guide you'll build a simple video call for two people. Then you'll add chat.

Time: about 10 minutes. Result: two browser tabs (or phones) that can see and hear each other.

The plan:

  1. Your backend makes a token for the logged-in user.
  2. Your app uses the token to join a room.
  3. The app turns on the camera and mic, and shows the other person.
Browser tabalice
Your backend
Livqeno
POST /join-room
checks who's logged in
make a token (API key)
grant
grant
join demo-room with the grant
camera + mic: live video and audio
bob sees alice
What happens when alice clicks Join.

Why the backend step? Your app can't be trusted with your secret API key, so your backend uses the key on the app's behalf and hands back a short-lived token instead. Think of a hotel: the API key is the master key that never leaves the front desk (your backend), and the token is a guest keycard for one room that expires. New to these terms? See What is Livqeno? or the Glossary.

What you need

  • A Livqeno account. Sign up in the dashboard. There are no servers to set up.
  • A backend (TypeScript, Python, Go, Java or PHP here) that knows who is logged in. Any login system works. Livqeno only needs a user id from it.
  • An app: a web page, React, React Native, or Flutter.

Create a project and an API key

A project holds everything for your app. The API key is your backend's secret for asking Livqeno to make tokens. Use a development key while you build.

  1. Create a project in the dashboard. It comes with a development environment ready to use.
  2. Create an API key. Use the dashboard, or the CLI (npm install -g @livqeno/cli):
Terminal
livqeno config set apiUrl https://api.livqeno.com   # once: the CLI defaults to localhost
livqeno login
livqeno projects list                               # copy your project's ID
livqeno projects use <project-id>                   # select it for the next command
livqeno keys create --name backend --environment development

You'll see the key only once. Copy it now:

Terminal
rvk_dev_8Kd2nQxwYtLm.aG9wZXlvdWFyZWhhdmluZ2Fsb3ZlbHlkYXk

Set two environment variables where your backend runs:

.env
LIVQENO_API_KEY=rvk_dev_...            # the key you just copied
LIVQENO_API_URL=https://api.livqeno.com

That's all the setup you need.

Install the SDKs

You need one package for your backend (it uses the API key) and one for your app (it uses tokens). Run each command in the matching project:

In your backend project (it holds the API key):

pnpm add @livqeno/server

In your browser app (it holds only a token):

pnpm add @livqeno/rtc

Mint a token on your backend

This code runs on your backend. It's the only place your API key is used. Add an endpoint (here /join-room) that your app calls when a user clicks Join. It does two things:

  1. Gets or creates a room. Both alice and bob call the same endpoint. getOrCreate: true makes the first call create demo-room and the second call get the same room back, instead of failing.
  2. Asks Livqeno for a token for the logged-in user, for that room, with the permissions they should have. Here: join, see others, and send camera and mic.
server.ts
import { Livqeno } from '@livqeno/server';
 
const livqeno = new Livqeno({
  apiKey: process.env.LIVQENO_API_KEY!,
  baseUrl: process.env.LIVQENO_API_URL!, // https://api.livqeno.com
});
 
app.post('/join-room', async (req, res) => {
  // Every participant calls this. getOrCreate returns the existing room
  // to the second caller instead of a 409.
  const room = await livqeno.rooms.create({ name: 'demo-room', getOrCreate: true });
 
  const grant = await livqeno.tokens.create({
    room: room.id,
    identity: req.user.id, // from your session, never from req.body
    permissions: { join: true, subscribe: true, publish: true },
    expiresIn: 600,
  });
 
  res.json(grant); // forward the whole object
});

Livqeno sends back an object like this (we call it the grant):

Response
{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "endpoint": "wss://api.livqeno.com/v1/rtc",
  "iceServers": [{ "urls": "stun:..." }, { "urls": "turn:...", "username": "...", "credential": "..." }],
  "telemetryUrl": "https://api.livqeno.com",
  "roomId": "8d86361a-7c01-4969-98cb-d0748360b803",
  "roomName": "demo-room",
  "participantIdentity": "user-42",
  "permissions": { "join": true, "subscribe": true, "publish": true, "publishAudio": true, "publishVideo": true, "publishData": false },
  "expiresAt": "2026-09-08T21:10:00.000Z"
}

Send the whole object to your app. Don't change it. The iceServers field lets calls work on mobile networks. If you remove or replace it, calls often work on Wi-Fi but fail on cellular.

More detail: Access tokens and RTC authentication.

Prefer plain HTTP? Here's the same request with curl:

Terminal
curl -X POST "$LIVQENO_API_URL/v1/rooms/$ROOM_ID/rtc-tokens" \
  -H "Authorization: Bearer $LIVQENO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "participantIdentity": "user-42", "permissions": { "join": true, "subscribe": true, "publish": true }, "ttlSeconds": 600 }'

Join the room and publish

This code runs in your app (browser or phone). It only uses the grant your backend returned, never the API key.

  1. Get the grant from your backend endpoint.
  2. Join the room.
  3. Turn on the camera and mic. Others see you right away. There's no separate publish() step.
call.ts
import { createRTCClient } from '@livqeno/rtc';
 
const grant = await fetch('/join-room', { method: 'POST' }).then((r) => r.json());
 
// createRTCClient reads token, endpoint, iceServers and telemetryUrl from the grant.
const client = createRTCClient(grant);
const room = await client.join(grant.roomName);
 
await room.enableCamera();
await room.enableMicrophone();

Listen for participants

This code also runs in your app. Joining only connects you. To see others, your app must show each person's video (their tracks) as it arrives.

  • People already in the room are ready as soon as join finishes.
  • People who arrive later fire an event.
call.ts
const grid = document.getElementById('grid')!;
 
for (const participant of room.remoteParticipants) {
  for (const track of participant.tracks) grid.append(track.attach());
}
 
room.on('participantJoined', (participant) => console.log(`${participant.identity} joined`));
 
// Fires when a remote track is actually playable — the one to render on.
room.on('trackSubscribed', (track, participant) => grid.append(track.attach()));
 
room.on('participantLeft', (participant) => console.log(`${participant.identity} left`));

All room events are listed in Events.

Leave

await room.leave(); // stops local tracks, closes the connection, tells the room

Call leave() when the user closes the page or the screen. Then others see participantLeft right away.

Done! Open the page in a second tab. You now have a working video call.

What just happened behind the scenes:

  1. Each tab called your backend, which made a token for that user.
  2. Each tab used its token to connect to Livqeno's media server.
  3. Turning on the camera published a video track. The other tab subscribed to it and fired trackSubscribed, so you could show it.

Add chat

Most call apps also want text chat: to share a link, or to talk when the mic is off. Chat in Livqeno is a separate product with its own token. Your backend makes it with the same API key. An RTC token doesn't work for chat, and a chat token doesn't work for calls.

Here we name the conversation demo-room too, so the call and its chat line up. Unlike a call, chat only lets in members, so your backend adds the user first.

On your backend:

  1. Get or create the conversation.
  2. Add the user as a member. (Safe to call every time.)
  3. Make a chat token for the user.
server.ts
app.post('/chat-token', async (req, res) => {
  // Create the conversation once (later callers get the same one back),
  // then make this user a member. A chat token only reaches conversations
  // its user belongs to; addMember is idempotent, so call it every time.
  await livqeno.chat.createConversation({ name: 'demo-room', getOrCreate: true });
  await livqeno.chat.addMember('demo-room', { userId: req.user.id });
 
  const chat = await livqeno.chat.createToken({
    userId: req.user.id,
    conversations: ['demo-room'],
  });
  res.json(chat); // { token, chatUrl, apiUrl, scopes, ... }
});

Then in your app, connect and send a message:

chat.ts
import { createChatClient } from '@livqeno/chat';
 
const grant = await fetch('/chat-token', { method: 'POST' }).then((r) => r.json());
const chat = createChatClient(grant);
 
// Join as you connect: a client subscribed to no conversation receives
// nothing, not even its own echo.
await chat.connect({ room: 'demo-room' });
 
chat.on('message', (message) => console.log(message.text));
await chat.sendMessage({ room: 'demo-room', text: 'hello from the quickstart' });

Full walkthrough: Chat quickstart.

Common errors

If something doesn't work, find what you see here first:

ErrorCauseFix
RAVEN_CONFLICT (409) on the second joinrooms.create or createConversation without getOrCreate.Pass getOrCreate: true (Python get_or_create=True).
ROOM_NOT_FOUND on join()The room passed to join() isn't the one the token was minted for.Pass grant.roomName or grant.roomId.
ROOM_NOT_FOUND on the first chat sendThe user isn't a member of the conversation.addMember(room, { userId }) before createToken.
ORIGIN_NOT_ALLOWEDThe page's origin isn't on the project's allowed list.Add it under Settings → Security → Allowed Origins.
TOKEN_EXPIREDTokens are short-lived.Mint a fresh one; a token's lifetime cannot be extended.
CAMERA_PERMISSION_DENIED / MICROPHONE_PERMISSION_DENIEDThe OS or browser refused device access.See Permissions.
Works on Wi-Fi, fails on cellulariceServers was dropped or replaced.Forward the grant's iceServers untouched.

More in RTC troubleshooting.

Next steps