@livqeno/server is for your TypeScript backend (plain JavaScript works too). It holds your secret
API key and talks to the Livqeno REST API for you.
Its most important job is making tokens. Your app (web,
React, React Native, or Flutter) can't join anything without one, and only
a backend with the API key can make one. So in a typical app, this SDK sits
behind an endpoint like POST /join-room:
- Your app calls your endpoint.
- Your endpoint checks who the user is (from your own login session).
- Your endpoint calls
livqeno.tokens.create(...)and returns the result. - Your app passes that result to its client SDK and joins.
It also manages rooms, chat conversations, and live streams, and reads metrics. It works the same way as the Python, Go, Java and PHP SDKs, so pick whichever matches your backend's language.
You need: a project and an API key. See API credentials.
Install
pnpm add @livqeno/servernpm install @livqeno/serveryarn add @livqeno/serverbun add @livqeno/serverSet up the client
On your backend, create one Livqeno instance when your server starts,
and reuse it for every request:
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
});Pass both values yourself. The SDK never reads process.env on its own.
This is on purpose: if it picked up variables silently, a production key
could end up in a development process without anyone noticing.
| Option | Required | What it's for |
|---|---|---|
apiKey | Yes | Your project API key. The SDK throws right away without it. |
baseUrl | Yes in production | https://api.livqeno.com. Unset, it falls back to http://localhost:4100, and refuses to start when NODE_ENV=production. |
timeout | No | Time limit per request, in milliseconds. Default 10000. |
maxRetries | No | How many times a temporary failure is retried. Default 2. |
What you can do
Here's the whole surface at a glance. Each call runs on your backend, usually inside one of your own HTTP endpoints. When you'd use each group:
| Group | You'd use it when… |
|---|---|
tokens | A user is about to join a call. Make one token per user, per join. |
rooms | You need a room for a meeting, a support ticket, or a game lobby. |
connections, errors, metrics, diagnostics | You're building an admin page or debugging why calls fail. |
chat | A user opens a chat. You create the conversation, add them, and make a chat token. |
liveStreams | You run webinars, classes, or broadcasts with one host and many viewers. |
transcriptions | You need live captions or a transcript of a room. See Live Transcription & Captions. |
agents | You want an AI voice agent to join a room. See AI Voice Agents. |
// Projects
await livqeno.projects.get(); // the one project this key belongs to
// RTC tokens — the core of this SDK
const token = await livqeno.tokens.create({
room: roomId,
identity: 'user-42',
permissions: { join: true, publish: true, subscribe: true },
expiresIn: 3600,
});
// Rooms
await livqeno.rooms.create({ name: 'demo-room', getOrCreate: true });
await livqeno.rooms.create({ name: 'board-meeting', e2ee: true }); // end-to-end encrypted media
await livqeno.rooms.list();
await livqeno.rooms.get(roomId);
await livqeno.rooms.participants.list(roomId); // null means the SFU couldn't be reached — distinct from a genuinely empty room ([])
// Observability
await livqeno.connections.list();
await livqeno.errors.list();
await livqeno.metrics.get('1h'); // '15m' | '1h' | '24h' | '7d'
await livqeno.diagnostics.get();
// Chat — see Chat Overview and the Python SDK page for the full surface
const conv = await livqeno.chat.createConversation({ name: 'support-room-42', getOrCreate: true });
await livqeno.chat.addMember(conv.publicId, { userId: 'alice' }); // a token only reaches conversations its user belongs to
await livqeno.chat.createToken({ userId: 'alice', conversations: [conv.publicId] });
// Live Streaming — see below
await livqeno.liveStreams.create({ title: 'Friday Q&A', hostIdentity: 'user-123' });Note: rooms.participants.list() returns null if the media server can't be
reached, and [] if the room is really empty. Treat null as "unknown", not
"nobody is here", or you might wrongly close a room that still has people in it.
getOrCreate: true matters when two users join at the same moment. Both
call your endpoint, both try to create demo-room, and without it the
second one gets RAVEN_CONFLICT (409). With it, the second one simply gets
the room that already exists.
e2ee: true creates an end-to-end encrypted room: the SDKs encrypt media
with keys Livqeno never sees, and server-side recording, HLS and restreaming
are refused for it. See End-to-end encryption.
Moderating a live call
These act on someone who is connected right now (otherwise
RAVEN_PARTICIPANT_NOT_CONNECTED, 404). roomId takes the room's id or
its name.
await livqeno.rooms.muteParticipant(roomId, 'alice', { kind: 'audio', muted: true }); // or { trackId, muted }
await livqeno.rooms.updateParticipantPermissions(roomId, 'alice', { publish: false }); // returns the result
await livqeno.rooms.removeParticipant(roomId, 'alice'); // doesn't revoke their token
// Metadata and attributes: the whole room gets participantMetadataChanged
await livqeno.rooms.updateParticipant(roomId, 'alice', { metadata: '{"role":"host"}', attributes: { hand: null } });
// A data message from the server (no sender participant on the receiving side)
await livqeno.rooms.sendData(roomId, 'meeting ends in 5 min', { topic: 'system' });tokens.create() also takes the participant's starting metadata and
attributes, and permissions.updateOwnMetadata lets the app change its
own (Metadata and attributes).
sendData() is never retried, so a retry can't deliver a message twice
(Data messages).
Webhooks
import { verifyWebhook } from '@livqeno/server';
const event = verifyWebhook(rawBody, req.header('swalvo-signature'), process.env.LIVQENO_WEBHOOK_SECRET!);Pass the raw body (a string or bytes, before any JSON parsing). It returns
the parsed event, or throws a LivqenoError with
RAVEN_WEBHOOK_INVALID_SIGNATURE, RAVEN_WEBHOOK_TIMESTAMP_OUT_OF_TOLERANCE
(older than toleranceSeconds, 300 by default) or
RAVEN_WEBHOOK_INVALID_PAYLOAD. See Webhooks.
Live streaming
A live stream has one main host, optional co-hosts, and many viewers who only watch (think webinar or a Friday Q&A). On your backend, the flow is: create the stream, start it, then give each person the right credential.
const stream = await livqeno.liveStreams.create({
title: 'Friday Q&A',
hostIdentity: 'user-123', // registered as this stream's HOST
});
await livqeno.liveStreams.start(stream.id);
// Registering a co-host mints full-publish RTC + moderator chat credentials
// in one call. Hand the result to the client SDK unchanged.
const hostCredential = await livqeno.liveStreams.addHost(stream.id, { identity: 'user-456' });
// A viewer token is always subscribe-only — there is no field here that
// can request publish access.
const viewerCredential = await livqeno.liveStreams.createViewerToken(stream.id, 'user-789');
await livqeno.liveStreams.removeHost(stream.id, 'user-456');
await livqeno.liveStreams.end(stream.id); // LIVE → ENDED, terminal| Method | What it does |
|---|---|
get() / list() | Read a stream's details and hosts. get() also returns the live viewer count. |
update() | Changes the stream's title, description, or other details. To change its status, use start() or end() instead. |
addHost() | Makes someone a co-host. Gives them publish + moderator chat access. |
createViewerToken() | Makes a watch-only token. It can never publish. |
leave() | Tells Livqeno a viewer left, so the live_stream.viewer_left webhook fires. |
end() | Ends the stream. An ended stream can't restart. |
liveStreams can also do three more things, each explained on its own page:
liveStreams.stage.invite(),.promote(), and.demote()bring a viewer up as a co-host. See Stage.liveStreams.restreamspushes the stream to other destinations, like YouTube or Twitch. See Restream.liveStreams.ingresslets a host go live from OBS or another encoder instead of a browser. See Ingest.
More: Live Streaming Overview and SDK Support Matrix.
Errors
Every failed call throws a LivqenoError. Log its requestId: if you contact
support, that id lets them find the exact request.
import { LivqenoError } from '@livqeno/server';
try {
await livqeno.rooms.get('missing-room');
} catch (error) {
if (error instanceof LivqenoError) {
console.log(error.code, error.statusCode, error.requestId);
}
}Errors never contain server stack traces, database errors, or RTC/TURN credentials. All codes: Error Codes.
Retries, timeouts, and lists
- Retries: only temporary failures are retried (network errors,
timeouts, 429, 502, 503, 504), with exponential backoff, up to
maxRetriestimes (default 2). Other 4xx errors are not retried, because sending the same bad request again would fail the same way. - Timeouts:
timeoutlimits every request (default 10 seconds), so a slow network never hangs your endpoint forever. - Lists: chat history uses cursor pages. Every other list returns a
plain array. You can cap it with
limit.
Security
The API key is stored in a private field (#apiKey). It never appears in
JSON.stringify(), logs, or errors. See Authentication.
Works with any framework
Express, Fastify, Next.js Route Handlers, serverless functions: any TypeScript or JavaScript server on a Node.js 20+ runtime. It's a plain HTTP client.
Common mistakes
| Mistake | What you'll see | Fix |
|---|---|---|
| Using this package in the browser | Your API key is exposed to every user. | Use it only on the server. In the browser, use @livqeno/rtc. |
Taking identity from the request body | Any user can join as anyone else. | Take the user id from your own login session. |
Creating a room without getOrCreate | RAVEN_CONFLICT (409) for the second person who joins. | Pass getOrCreate: true. |
| Changing the token response before sending it | Calls work on Wi-Fi but fail on mobile data. | Send the whole object to your app as-is. |
Forgetting baseUrl | Requests go to localhost:4100, or the SDK won't start in production. | Set LIVQENO_API_URL and pass it. |
Next steps
- Build the full token endpoint and a working call: Quickstart.
- Learn what you can put in a token: Access tokens.
- Set up the app side: Next.js, React, React Native, or Flutter.
- Calling the REST API directly too? Retry it safely with Idempotency.