In this guide you'll run a complete live stream: alice hosts a "Launch Day"
broadcast, and carol watches, chats, and sends a ❤️. You can run both sides
in two browser tabs.
Where this fits: this is the Live Streaming version of the main Quickstart. The idea is the same: your backend makes credentials with the API key, and your app joins with them. The difference is that one call gives each person two tokens (video and chat), and the role (host or viewer) decides whether they can broadcast.
The plan:
| Step | Where it runs | What happens |
|---|---|---|
| 1. Create a stream | Backend | Livqeno makes the room and the chat, and records alice as host. |
| 2. Make the host's credentials | Backend | Alice gets tokens that can broadcast. |
| 3. Join as host and go live | App, then backend | Alice turns on her camera. Your backend marks the stream LIVE. |
| 4. Make a viewer credential and join | Backend, then app | Carol gets watch-only tokens and joins. |
| 5. Chat and react | App | Carol and Alice talk in the stream chat. |
| 6. End the stream | Backend | Everyone is disconnected. The stream is finished for good. |
What you need:
- A Livqeno project and an API key. See API credentials.
- A backend (TypeScript, Python, Go, Java, PHP, or anything that can send HTTP) with
LIVQENO_API_KEYandLIVQENO_API_URLset. - A client SDK in your app. See Install an SDK.
On the web, Live Streaming lives in
@livqeno/live.
1. Create a stream
On your backend, create the stream. Do this when your user schedules or opens an event, for example when Alice clicks Create event in your app.
This one call does three things: it creates the video room, creates the chat
conversation, and records alice as the stream's HOST:
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
});
const stream = await livqeno.liveStreams.create({ title: 'Launch Day', hostIdentity: 'alice' });
// { id: 'stream_jRoD1T3EXh0PMJRGG4zYzQ', status: 'CREATED',
// conversationId: 'conv_0iojWXJCtXZUUkDR7THBmQ',
// chatRootMessageId: 'msg_Efm2zArYJTSRUr88BV5bZg',
// hosts: [{ identity: 'alice', role: 'HOST' }], ... }import os
from livqeno import Livqeno, CreateLiveStreamParams
livqeno = Livqeno(
api_key=os.environ["LIVQENO_API_KEY"],
base_url=os.environ["LIVQENO_API_URL"], # https://api.livqeno.com
)
stream = livqeno.live_streams.create(CreateLiveStreamParams(title="Launch Day", host_identity="alice"))client := livqeno.New(
os.Getenv("LIVQENO_API_KEY"),
livqeno.WithBaseURL(os.Getenv("LIVQENO_API_URL")), // https://api.livqeno.com
)
stream, err := client.LiveStreams.Create(ctx, livqeno.CreateLiveStreamParams{Title: "Launch Day", HostIdentity: "alice"})Livqeno livqeno = Livqeno.builder()
.apiKey(System.getenv("LIVQENO_API_KEY"))
.baseUrl(System.getenv("LIVQENO_API_URL")) // https://api.livqeno.com
.build();
LiveStream stream = livqeno.liveStreams().create(
CreateLiveStreamParams.builder().title("Launch Day").hostIdentity("alice").build());$client = new \Livqeno\Client(
apiKey: getenv('LIVQENO_API_KEY'),
baseUrl: getenv('LIVQENO_API_URL'), // https://api.livqeno.com
);
$stream = $client->liveStreams->create(['title' => 'Launch Day', 'hostIdentity' => 'alice']);curl -X POST "$LIVQENO_API_URL/v1/live-streams" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Launch Day", "hostIdentity": "alice"}'The room that call creates belongs to the stream. It is not listed with the
rooms you make yourself (rooms.list(), and the Rooms page in the
dashboard), because you run it through the stream, from its own page, never as
a separate room. Closing a room disconnects everyone in it, which for a
stream's room would cut the broadcast.
Save stream.id (it looks like stream_...). Every later call uses it.
The stream is now CREATED: it exists, but nobody is live yet.
2. Mint the host's credentials
Creating the stream recorded Alice as host, but it didn't give her anything to
join with. On your backend, call addHost to get her credentials. Do this
when Alice opens the host screen, after your own check that she is allowed to
host this event:
const hostCredential = await livqeno.liveStreams.addHost(stream.id, { identity: 'alice' });
// { identity: 'alice', role: 'HOST', rtc: {...}, chat: {...} }from livqeno import AddHostParams
host_credential = livqeno.live_streams.add_host(stream["id"], AddHostParams(identity="alice"))hostCredential, err := client.LiveStreams.AddHost(ctx, stream.ID, livqeno.AddHostParams{Identity: "alice"})IssuedStreamCredential hostCredential = livqeno.liveStreams().addHost(stream.id(), AddHostParams.of("alice"));$hostCredential = $client->liveStreams->addHost($stream->id, ['identity' => 'alice']);curl -X POST "$LIVQENO_API_URL/v1/live-streams/stream_jRoD1T3EXh0PMJRGG4zYzQ/hosts" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity": "alice", "role": "HOST"}'You get back one object with two tokens inside:
rtc: a video token that can broadcast (publish camera and mic), andchat: a chat token withADMINscope, so Alice can moderate.
It also has streamId, role and chatRootMessageId (used for reactions).
We left out role in the SDK calls above. That's fine here: Alice is already
registered as HOST, and leaving role out keeps a person's existing role.
For someone new, leaving it out makes them a CO_HOST.
Send this whole response to Alice's app, unchanged. Your app passes it
straight to LiveStream.join(). Don't pick fields out or rebuild it.
3. Join as host
In Alice's app, join with the credentials from step 2 and turn on the camera and mic. As soon as a device is on, it's being broadcast into the room. There's no separate "publish" step:
import { LiveStream } from '@livqeno/live';
const stream = await LiveStream.join(credentials); // exactly what addHost() returned: { streamId, role, rtc, chat, chatRootMessageId }
await stream.room.enableCamera();
await stream.room.enableMicrophone();stream.room is a normal @livqeno/rtc Room. stream.chat is a
normal @livqeno/chat client. Everything you know about them works here.
'use client';
import { LivqenoLiveStream, useLiveStreamHost, useParticipants } from '@livqeno/react';
function HostPage({ credentials }) {
return (
<LivqenoLiveStream credentials={credentials} fallback={<p>Connecting…</p>}>
<HostControls />
</LivqenoLiveStream>
);
}
function HostControls() {
const { camera, microphone } = useLiveStreamHost();
const participants = useParticipants(); // every existing hook already works inside <LivqenoLiveStream>
return (
<button onClick={() => camera.enable()}>Go live ({participants.length} in room)</button>
);
}There's no useLiveStreamParticipants(). Just use useParticipants().
The stream's room is a normal Room.
import { joinLiveStream } from '@livqeno/react-native';
const stream = await joinLiveStream(credentials);
await stream.room.enableCamera();
await stream.room.enableMicrophone();joinLiveStream() asks for camera and mic permission only if the role can
broadcast. Hosts get asked. Viewers don't.
import 'package:livqeno_live/livqeno_live.dart';
final stream = await LivqenoLiveStream.join(credentials);
await stream.room.enableCamera();
await stream.room.enableMicrophone();Then, on your backend, start the stream. Call this when Alice clicks Go live in your app:
await livqeno.liveStreams.start(stream.id);livqeno.live_streams.start(stream["id"])stream, err = client.LiveStreams.Start(ctx, stream.ID)livqeno.liveStreams().start(stream.id());$client->liveStreams->start($stream->id);curl -X POST "$LIVQENO_API_URL/v1/live-streams/stream_jRoD1T3EXh0PMJRGG4zYzQ/start" \
-H "Authorization: Bearer $LIVQENO_API_KEY"Now the stream is LIVE:
- Your backend gets the
live_stream.startedwebhook. You could use it to notify followers. - Starting is where the "one live stream at a time" free-tier limit is
checked. A second
start()while another stream is live fails withRAVEN_STREAM_CONCURRENCY_LIMIT_EXCEEDED. - The 240-minute stream limit counts from now.
4. Mint a viewer token and join
On your backend, make a viewer credential for each person who opens the stream page. Here, Carol opens it:
const viewerCredential = await livqeno.liveStreams.createViewerToken(stream.id, 'carol');viewer_credential = livqeno.live_streams.create_viewer_token(stream["id"], "carol")viewerCredential, err := client.LiveStreams.CreateViewerToken(ctx, stream.ID, "carol", nil)IssuedStreamCredential viewerCredential = livqeno.liveStreams().createViewerToken(stream.id(), "carol");$viewerCredential = $client->liveStreams->createViewerToken($stream->id, 'carol');curl -X POST "$LIVQENO_API_URL/v1/live-streams/stream_jRoD1T3EXh0PMJRGG4zYzQ/viewer-tokens" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity": "carol"}'A viewer credential can only watch. No field on this call can let a viewer broadcast. This is on purpose: it means a viewer can never take over the stream, whatever their app sends.
You get back the same shape as the host credential, with role: 'VIEWER'.
Send it to Carol's app unchanged.
Then, in Carol's app, join and show the host's video. A viewer doesn't turn on any devices. They only receive:
const stream = await LiveStream.join(credentials); // role: 'VIEWER'
stream.room.on('trackSubscribed', (track, participant) => {
if (track.kind === 'camera') track.attach(videoEl);
});'use client';
import { useLiveStreamViewer, useRemoteParticipants } from '@livqeno/react';
function ViewerControls() {
const { react } = useLiveStreamViewer();
const remote = useRemoteParticipants();
return <button onClick={() => react('❤️')}>❤️ ({remote.length} watching)</button>;
}const stream = await joinLiveStream(credentials); // role: 'VIEWER'
const { room } = useLiveStream(stream);final stream = await LivqenoLiveStream.join(credentials); // role: VIEWER
for (final participant in stream.room.remoteParticipants) {
// build a LivqenoVideoView per participant
}trackSubscribed fires when the host's video is ready to play. If Carol
joined before Alice turned her camera on, it fires the moment Alice does.
5. Chat and react
In either app, host or viewer, use stream.chat to send and read messages
and stream.react() to send an emoji. stream.chat is a normal Livqeno chat
client, already connected to this stream's conversation:
await stream.chat.sendMessage({ text: 'hey!' });
stream.chat.on('message', (m) => console.log(m.senderId, m.text));
await stream.react('❤️');
stream.chat.on('reactionAdded', (e) => console.log(e.userId, e.emoji));await stream.chat!.send('hey!');
await stream.react('❤️');await stream.chat?.send('hey!');
await stream.react('❤️');6. End the stream
On your backend, end the stream when Alice clicks End stream, or when your event is over:
await livqeno.liveStreams.end(stream.id);livqeno.live_streams.end(stream["id"])stream, err = client.LiveStreams.End(ctx, stream.ID)livqeno.liveStreams().end(stream.id());$client->liveStreams->end($stream->id);curl -X POST "$LIVQENO_API_URL/v1/live-streams/stream_jRoD1T3EXh0PMJRGG4zYzQ/end" \
-H "Authorization: Bearer $LIVQENO_API_KEY"This:
- closes the room and disconnects everyone still connected, host and viewers, and
- sends the
live_stream.endedwebhook.
You can't call start or end again on the same stream, and you can't make
new credentials for it. Status only moves forward. For the next event, create
a new stream.
If you forget to end a stream, Livqeno ends it for you after 240 minutes of
being LIVE.
Done! You ran a full live stream.
What Livqeno handles vs. what you handle
| Livqeno handles | You handle |
|---|---|
| The video room and the media servers | Deciding who is allowed to host (your own permission check before addHost) |
| The chat conversation | Deciding who may watch (your own check before createViewerToken) |
| Making sure viewers can never broadcast, on any SDK | The UI: video player, chat panel, buttons |
| Ending streams that run past the time limit | When to call start() and end() |
Common errors
| Error | Cause | Fix |
|---|---|---|
RAVEN_STREAM_NOT_FOUND | Wrong streamId, or the stream is in another project or environment. | Use the id create() returned. |
RAVEN_STREAM_INVALID_STATE | start() on a stream that isn't CREATED, or end() on one that isn't LIVE. | Check stream.status first. Status only moves forward. |
Viewer's room.enableCamera() throws PERMISSION_DENIED | This is expected. Viewer tokens always have publish: false. | Make a host or co-host credential instead. |
RAVEN_STREAM_INVALID_STATE when making a credential | The stream has ENDED. | Create a new stream. |
RAVEN_STREAM_VIEWER_LIMIT_EXCEEDED | 100 viewers are already connected. | Wait for someone to leave. See Free tier. |
RAVEN_STREAM_CONCURRENCY_LIMIT_EXCEEDED on start() | The account already has a LIVE stream. The limit is per account, across every project and environment, so a stream your app started earlier and never ended (a crash, a closed tab, a test run) counts. The error message names the stream. | End it: livqeno.liveStreams.end(id). To find strays, list streams and end the ones still LIVE. See Clean up a stuck stream. |
stream.react() throws | The credentials passed to join() were missing chat or chatRootMessageId. | Pass the whole addHost / createViewerToken response, unchanged. |
Clean up a stuck stream
A stream stays LIVE until your backend ends it; closing the host's app does
not. On the free tier that blocks the next start(), so end strays before
starting a new one, and in tests:
const live = (await livqeno.liveStreams.list()).filter((s) => s.status === 'LIVE');
for (const s of live) await livqeno.liveStreams.end(s.id);A stream also ends by itself when it reaches the 240-minute limit.
Production tips
- Never call
livqeno.liveStreams/livqeno.live_streamsfrom a browser or app. They need your API key. addHost()andcreateViewerToken()decide the user's role. The role depends only on which one your backend calls, never on anything the app sends.- Always include
chatRootMessageIdin the join credentials.stream.react()needs it.
Full working example
See examples/live-streaming-demo in the Livqeno repo. It's a complete
host and viewer demo in two tabs:
- a FastAPI backend that makes the credentials, and
- a plain HTML/JS frontend using the calls above (no bundler).
Next steps
You've seen the whole flow. To build it into your app:
- Streams & lifecycle: list, update, and end streams from your backend.
- Hosts & co-hosts: bring a guest on stage.
- Viewers: build the watch page.
- Stream events: react to webhooks on your backend.
- SDK support: check what your platform can do.