Your backend often needs to react when something happens in a stream: post "We're live!" when it starts, keep a list of who attended, or close the event page when it ends. Livqeno tells your backend with webhooks.
Where this fits: a webhook is an HTTP POST that Livqeno sends to a URL
on your backend. It works the same for every Livqeno product. If you haven't
set one up yet, start with Webhooks. This page lists the
live-stream events and what's in them.
Things that happen inside the app, like a new chat message on screen, come as client events instead. See Client events.
The seven events
| Event | Sent when |
|---|---|
live_stream.created | A stream is created (not live yet) |
live_stream.started | start() moved it from CREATED to LIVE |
live_stream.ended | end() moved it from LIVE to ENDED (final) |
live_stream.host_joined | A host or co-host was added and got credentials |
live_stream.host_left | A co-host was removed |
live_stream.viewer_joined | A viewer token was made |
live_stream.viewer_left | Your backend called leave() for a viewer |
Two more events exist for streams using the broadcast delivery mode:
live_stream.broadcast_ready and live_stream.egress_failed. See the
Event catalogue.
Restream and encoder ingest add five more:
| Event | Sent when | data fields |
|---|---|---|
live_stream.restream_active | A restream destination started receiving video | streamId, restreamId, label, at |
live_stream.restream_failed | A restream destination reached FAILED | streamId, restreamId, label, reason, at |
live_stream.ingress_connected | An OBS or encoder ingest started publishing | streamId, ingressId, protocol, at |
live_stream.ingress_disconnected | The encoder left and didn't come back within the reconnect window | streamId, ingressId, protocol, reason, at |
live_stream.ingress_failed | Livqeno refused the encoder's media | streamId, ingressId, protocol, reason, at |
Note that host_joined and viewer_joined fire when your backend makes
the credential, not when the person actually connects.
Subscribe
Register a webhook endpoint once per environment, from your terminal or a setup script. List the events you want. Leave the list empty to get all of them:
curl -X POST "$LIVQENO_API_URL/v1/projects/$PROJECT_ID/webhooks" \
-H "Authorization: Bearer $LIVQENO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"url": "https://api.example.com/hooks/livqeno",
"environment": "PRODUCTION",
"events": ["live_stream.started", "live_stream.ended"]
}'Payloads
Each event uses the standard webhook envelope. The
stream details are in data. These are the exact fields:
| Event | data fields |
|---|---|
live_stream.created | streamId, title, visibility, hostIdentity, createdAt |
live_stream.started | streamId, startedAt |
live_stream.ended | streamId, endedAt, durationMs (null if it never started) |
live_stream.host_joined | streamId, identity, role, at |
live_stream.host_left | streamId, identity, at |
live_stream.viewer_joined | streamId, identity, at |
live_stream.viewer_left | streamId, identity, at |
title is only on created. Need it later? Save it then, or read it with
liveStreams.get().
{
"id": "evt_GSyoZrH7qF3tZms0QsW4Gw",
"type": "live_stream.created",
"projectId": "3e48ccb1-...",
"environment": "PRODUCTION",
"createdAt": "2026-09-08T12:00:00.000Z",
"data": {
"streamId": "stream_jRoD1T3EXh0PMJRGG4zYzQ",
"title": "Friday Q&A",
"visibility": "PUBLIC",
"hostIdentity": "user-1",
"createdAt": "2026-09-08T12:00:00.000Z"
}
}New fields may be added to data. Ignore fields you don't know instead of failing.
viewer_left only fires if you call leave()
viewer_joinedfires when your backend makes a viewer token.viewer_leftfires only when your backend calls:
await livqeno.liveStreams.leave(stream.id, 'user-99');Client events (not webhooks)
What happens inside the stream comes as client events in your app, on the room and the chat. Use these to update the screen:
| You want to know when… | Use |
|---|---|
| A video arrived | stream.room.on('trackSubscribed', …) |
| Someone joined the room | stream.room.on('participantJoined', …) |
| A chat message arrived | stream.chat.on('message', …) |
| A reaction arrived | stream.chat.on('reactionAdded', …) |
| The connection dropped | stream.room.on('connectionStateChanged', …) |
stream.room is a normal Room and stream.chat is a normal ChatClient.
So RTC events and Chat events work the same.
There's nothing new to learn.
Verify and remove duplicates
If your endpoint is slow or down, Livqeno sends the event again later. So follow the same rules as every webhook:
- Check the signature.
- Skip duplicates using
Livqeno-Event-Id. - Order by
createdAt. - Reply with a 2xx within 5 seconds.
See Handle webhooks.
live_stream.ended can arrive twice if it's retried. Make sure handling
it twice is safe.
Next steps
- Handle webhooks: build the endpoint that receives these.
- Event catalogue: every Livqeno event, all products.
- Analytics: use these events to track attendance.