livqenoDocs

BackendWebhooks

Webhooks

Get events sent to your backend. Signed, retried, and kept per environment.

A webhook is how Livqeno tells your backend that something happened, like a user joining a call, a message being sent, or a stream ending. You give Livqeno a URL on your server, and it sends an HTTP POST to that URL for each event.

Why use them? Your app only knows what happens while it's open. Your backend often needs to know even when no one is looking. For example:

  • Keep your member list in sync when someone joins or leaves a chat conversation (participant.joined / participant.left).
  • Send a push notification when a message arrives for someone offline (message.created).
  • Mark a webinar as finished in your database (live_stream.ended).

Where this fits: project → API key → your app joins → Livqeno sends webhooks to your backend. You need a backend that can receive HTTPS requests from the internet.

Webhooks are set per project. One setup covers every event source. See the Event catalogue for the events you can get.

Add an endpoint

An endpoint is one URL that receives webhooks. Set it up once per environment:

  1. In the dashboard, open your project → Webhooks → Add endpoint.
  2. Enter your HTTPS URL.
  3. Pick the environment.
  4. Pick the events you want. Leave the list empty to get all of them.
  5. Copy the signing secret. It's shown only once. Save it with your other server secrets.

Each endpoint belongs to one environment. A staging endpoint never gets production events, and the other way around. This is checked before anything is sent. See Environments.

The payload

Every webhook is a JSON body with the same shape. type tells you what happened, and data holds the details for that event type:

{
  "id": "evt_GSyoZrH7qF3tZms0QsW4Gw",
  "type": "message.created",
  "projectId": "3e48ccb1-...",
  "environment": "PRODUCTION",
  "createdAt": "2026-08-18T12:00:00.000Z",
  "data": { "message": { "id": "msg_3xR…", "roomId": "conv_9WcQ…", "senderId": "alice", "text": "Hello everyone!" } }
}

It comes with these headers:

HeaderContains
Swalvo-Signaturet=<unix>,v1=<hmac-sha256>
Livqeno-Event-IdThe evt_... id. Use it to skip duplicates.
Livqeno-Event-TypeThe event type

Every delivery also carries the same values under the legacy names Raven-Signature, Raven-Event-Id and Raven-Event-Type, so receivers written before the rename keep working. Read the Livqeno-* header first and fall back to Raven-*; the SDK verifiers already do this.

Verifying a delivery

Each request is signed with your endpoint's signing secret. On your backend, recompute the signature and compare it before you trust the event. If it doesn't match, reject the request (the webhook guide replies 400) and ignore it:

import { createHmac, timingSafeEqual } from 'node:crypto';
 
function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  const timestamp = Number(parts.t);
 
  // Reject anything older than five minutes — this is what stops a
  // captured delivery being replayed later.
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return false;
  }
 
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(parts.v1 ?? '', 'utf8');
  const b = Buffer.from(expected, 'utf8');
 
  // Length check first: timingSafeEqual throws on a mismatch, and a plain
  // === leaks the expected signature one byte at a time.
  return a.length === b.length && timingSafeEqual(a, b);
}

Every server SDK ships this check: the same HMAC over <t>.<raw body>, a constant-time compare and the 300-second window. The TypeScript and Python helpers also accept a header with several v1= signatures (any match passes) and a list of secrets, for rotating one.

import express from 'express';
import { verifyWebhook } from '@livqeno/server';
 
app.post('/webhooks/livqeno', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.header('swalvo-signature'), process.env.LIVQENO_WEBHOOK_SECRET!);
  } catch {
    return res.sendStatus(400); // LivqenoError: RAVEN_WEBHOOK_INVALID_SIGNATURE, ..._TIMESTAMP_OUT_OF_TOLERANCE
  }
  // event.id (dedupe on it), event.type, event.data
  res.sendStatus(200);
});

verifyWebhook(rawBody, signatureHeader, secret, { toleranceSeconds }) returns the parsed event or throws a LivqenoError.

Three common mistakes:

  1. Using the parsed body. Sign the raw body. JSON.parse then JSON.stringify can change key order and spaces, so the signature won't match. Grab the raw bytes before your JSON middleware runs.
  2. Skipping the time check. The timestamp is part of what's signed. Rejecting old timestamps (over 5 minutes) stops someone from re-sending a captured request later.
  3. Comparing with ===. Use a constant-time compare like timingSafeEqual. A plain === can leak the signature through timing.

Retries

Your endpoint will sometimes be down: a deploy, a crash, a slow database. Livqeno doesn't drop the event; it tries again later.

Sending a webhook never slows down your users. For example, a message is saved and delivered to users before its webhook is even queued.

If your endpoint fails (any non-2xx status, a timeout, or a connection error), Livqeno retries with growing waits:

10s → 20s → 40s → 80s → 160s → 320s, then it's marked FAILED.

After 50 failures in a row (across all deliveries), the endpoint is turned off automatically. Fix it, then turn it back on in the dashboard. That also resets the failure count.

Idempotency and ordering

  • You will get duplicates. A delivery can succeed but the reply gets lost, or a worker can restart. Skip duplicates using Livqeno-Event-Id. It stays the same on every retry of the same event.
  • Events can arrive out of order. A retried event can arrive after a newer one. Sort by createdAt in the payload, not by arrival time. Don't assume events come in sequence.

Responding

Reply with a 2xx fast, then do the real work in the background (for example, put the event in a job queue and return 200).

The timeout is 5 seconds. A slower reply counts as a failure and is retried, so slow work turns into duplicates.

Security

  • The signing secret is shown once, when you create the endpoint. It's never shown again. Livqeno does store it, because it needs it to sign each delivery. To rotate it, delete the endpoint and create a new one.
  • Internal addresses are blocked. Livqeno refuses non-HTTP schemes, loopback, private-range, link-local and cloud-metadata addresses (except in local development). Production also requires https://.
  • The check runs on every delivery, not only when you save the URL. Livqeno resolves your hostname when it sends, and refuses the delivery if any address it resolves to is internal. It connects to the address it checked, so the name can't be re-pointed in between.
  • Redirects are not followed. A 3xx reply counts as a failed delivery and is retried. If your endpoint moved, update its URL.
  • One slow endpoint doesn't delay the others. Each endpoint is delivered to independently, oldest event first.

Next steps