livqenoDocs

SDKsJava

Java SDK

Make tokens and manage rooms, chat and streams from your JVM backend. Java 17+, synchronous and thread-safe.

The Java SDK is for your JVM backend (Spring Boot, Quarkus, Micronaut, a servlet container, or Kotlin). It holds your secret API key and talks to the Livqeno REST API for you.

Its main job is making tokens. Your app (web, React, React Native, Flutter, iOS or Android) can't join a call or chat without one, and only a backend holding the API key can make one. In a typical app, this SDK sits behind an endpoint like POST /join-room: your app calls it, your code checks who the user is, and this SDK returns a token for that user.

It mirrors the TypeScript SDK resource for resource, with Java names: resources are accessor methods (livqeno.liveStreams()), parameters are builders, and responses are immutable records.

You need: Java 17 or later, a project, and an API key. See API credentials.

Install

The SDK is not on Maven Central yet. Build it from sdks/java in the repository with mvn install, then add the dependency:

pom.xml
<dependency>
  <groupId>com.livqeno</groupId>
  <artifactId>livqeno-server</artifactId>
  <version>0.1.0</version>
</dependency>

Gradle: implementation("com.livqeno:livqeno-server:0.1.0").

It uses java.net.http.HttpClient. Its only runtime dependency is jackson-databind.

Set up the client

On your backend, build one client at startup and share it. Instances are immutable and thread-safe.

LivqenoConfig.java
import com.livqeno.server.Livqeno;
 
Livqeno livqeno = Livqeno.builder()
        .apiKey(System.getenv("LIVQENO_API_KEY"))
        .baseUrl(System.getenv("LIVQENO_API_URL"))   // https://api.livqeno.com
        .build();

Pass the API key yourself. The SDK never reads it from the environment on its own.

Builder methodDefaultNotes
apiKey(String)requiredNever read from the environment implicitly.
baseUrl(String)http://localhost:4100Required in production. A trailing / is trimmed.
timeout(Duration)10 sPer attempt.
maxRetries(int)2Retries network errors, timeouts and 429/502/503/504.
httpClient(HttpClient)built-inFor proxies or executors. Don't let it follow redirects.

Production base URL. Without baseUrl the SDK falls back to a local dev stack. So build() throws RAVEN_INVALID_CONFIG when there's no baseUrl and the environment is production: the livqeno.env system property, or the LIVQENO_ENV or NODE_ENV environment variable, equals production.

RTC tokens

Call this when a user is about to join a call, once per user, per join. Take identity from your own login session, never from the request body, or any user could join as anyone.

JoinController.java
import com.livqeno.server.model.*;
 
@PostMapping("/join-room")
IssuedToken joinRoom(@AuthenticationPrincipal User user) {
    // Every participant calls this. getOrCreate returns the existing room
    // to the second caller instead of a 409.
    Room room = livqeno.rooms().create(CreateRoomParams.builder().name("demo-room").getOrCreate(true).build());
 
    return livqeno.tokens().create(CreateTokenParams.builder()
            .room(room.id())
            .identity(user.getId())                      // from your session, never the body
            .permissions(TokenPermissions.builder().join(true).subscribe(true).publish(true).build())
            .expiresIn(600)                              // seconds, 30 to 21600
            .build());                                   // forward the whole object
}

IssuedToken is a record whose components are named like the wire fields (token(), endpoint(), iceServers(), telemetryUrl(), expiresAt()), so Jackson serializes it back to the same JSON the TypeScript SDK returns. Return it whole. The iceServers part is what lets calls work on mobile networks.

Moderating a live call

These methods only work on a participant who is connected to the call right now. If they're not connected, you'll get a 404 with the code RAVEN_PARTICIPANT_NOT_CONNECTED. For roomId, you can pass either the room's id or its name.

livqeno.rooms().participants().mute("demo-room", "alice",
        MuteParticipantParams.builder().kind(TrackKind.AUDIO).muted(true).build()); // or .trackId(...)
 
livqeno.rooms().participants().updatePermissions("demo-room", "alice",
        UpdateParticipantPermissionsParams.builder().publish(false).build());        // returns the result
 
livqeno.rooms().participants().remove("demo-room", "alice");                          // doesn't revoke their token

participants().list(roomId) and .get(roomId, identity) return an empty Optional when they can't reach the SFU (the server that routes the call's audio and video). That's different from a room that's genuinely empty, which still comes back as an Optional holding an empty list. See Rooms & participants for more.

Chat

Your backend creates conversations, decides who is a member, and mints a chat token for each user. The browser never creates conversations or adds members itself.

ChatConversation conv = livqeno.chat().createConversation(CreateConversationParams.builder()
        .name("support-room-42").type(ChatConversationType.CHANNEL)
        .addMember("alice", ChatMemberRole.ADMIN)        // a conversation with no members is unusable
        .addMember("bob", ChatMemberRole.MEMBER)
        .getOrCreate(true).build());
 
IssuedChatToken chatToken = livqeno.chat().createToken(CreateChatTokenParams.builder()
        .userId(user.getId()).conversations(conv.publicId())
        .scopes(ChatScope.READ, ChatScope.SEND).expiresIn(900).build());
// Hand chatToken.token() to the browser. Nothing else.

Members, messages and moderation:

livqeno.chat().addMember("support-room-42", ChatMemberParams.of("carol", ChatMemberRole.MODERATOR));
livqeno.chat().removeMember("support-room-42", "carol");
 
livqeno.chat().sendMessage("support-room-42", SendChatMessageParams.builder()
        .senderId("system").type(ChatMessageType.SYSTEM).text("Maintenance in 5 minutes")
        .clientMessageId("maint-1").build());           // idempotency key
 
ChatMessagePage page = livqeno.chat().listMessages("support-room-42",
        ListChatMessagesParams.builder().limit(50).build());
livqeno.chat().deleteMessage("msg_abc");                // soft delete

Live streaming

A live stream is one main host, optional co-hosts, and many viewers who only watch. On your backend: create the stream, start it, then give each person the right credential.

LiveStream stream = livqeno.liveStreams().create(CreateLiveStreamParams.builder()
        .title("Friday Q&A").hostIdentity("user-123").build());
livqeno.liveStreams().start(stream.id());               // CREATED -> LIVE
 
// Registering a co-host mints full-publish RTC + moderator chat
// credentials in one call. Hand the result to the client SDK unchanged.
IssuedStreamCredential host = livqeno.liveStreams().addHost(stream.id(), AddHostParams.of("user-456"));
 
// A viewer credential is always subscribe-only.
IssuedStreamCredential viewer = livqeno.liveStreams().createViewerToken(stream.id(), "user-789");
 
livqeno.liveStreams().removeHost(stream.id(), "user-456");
livqeno.liveStreams().end(stream.id());                 // LIVE -> ENDED, terminal
  • livqeno.live() returns the same object as livqeno.liveStreams().
  • The method you call decides the role: addHost() makes a host, createViewerToken() makes a viewer. Nothing in the request can change that.
  • For a BROADCAST stream, pass ViewerTokenOptions.acceptingPlayback() and read playback().llhlsUrl() (or playback().url()) instead of getting a 409. See Viewers.
  • livqeno.liveStreams().stage(), .restreams() and .ingress() cover the stage, restreaming and OBS ingest.

Webhooks

Livqeno signs every delivery with swalvo-signature: t=<unix>,v1=<hmac>. Verify it against the raw body bytes, before you parse any JSON. WebhookVerifier compares in constant time and rejects timestamps more than 300 s off.

WebhookServlet.java
import com.livqeno.server.webhooks.WebhookVerifier;
 
byte[] raw = request.getInputStream().readAllBytes();
String signature = request.getHeader(WebhookVerifier.SIGNATURE_HEADER);   // "swalvo-signature"
if (!WebhookVerifier.verify(raw, signature, System.getenv("LIVQENO_WEBHOOK_SECRET"))) {
    response.setStatus(400);
    return;
}
String eventId   = request.getHeader(WebhookVerifier.EVENT_ID_HEADER);    // dedupe on it
String eventType = request.getHeader(WebhookVerifier.EVENT_TYPE_HEADER);  // e.g. "live_stream.started"

There are also overloads that take an explicit nowSeconds and tolerance. WebhookVerifier.sign(body, secret, t) produces a header exactly the way Livqeno does, which helps when you test your endpoint. More: Webhooks.

Errors

The SDK throws one unchecked exception type, LivqenoException. Branch on getCode(), and log getRequestId(): support can use it to find the exact request.

import com.livqeno.server.LivqenoException;
 
try {
    livqeno.rooms().get("2f1b6c3e-8d4a-4b7e-9c21-5a0e7d3f9b10");   // room ids are UUIDs
} catch (LivqenoException e) {
    switch (e.getCode()) {                    // e.g. "RAVEN_ROOM_NOT_FOUND"; never null
        case "RAVEN_ROOM_NOT_FOUND" -> log.warn("no such room, request {}", e.getRequestId());
        case "RAVEN_RATE_LIMITED"   -> retryLater();
        default -> throw e;
    }
}

getStatusCode() is the HTTP status, or null for config, timeout and network errors. The SDK produces RAVEN_INVALID_CONFIG, RAVEN_TIMEOUT and RAVEN_NETWORK_ERROR itself, and RAVEN_INVALID_RESPONSE when a 2xx body didn't decode. All codes: Error Codes.

Retries, timeouts, and lists

SettingBehavior
RetriesNetwork errors, timeouts, 429, 502, 503 and 504 only, with exponential backoff from 300 ms. maxRetries defaults to 2. Other 4xx errors are never retried.
Timeoutstimeout(Duration), default 10 s, per attempt.
ListsChat history uses cursors (page.hasMore(), page.nextCursor()). Every other list returns a plain List.

Enum fields decode an unknown value from a newer API as UNRECOGNIZED instead of throwing, and unknown JSON fields are ignored.

Security

The API key never appears in toString() on the client, its builder or its resources, or in any exception. If a server ever echoes the key back, it is replaced with [REDACTED]. IngressWithKey and CreateRestreamParams also leave stream keys out of their toString(). See Authentication.

Next steps

  1. Build a token endpoint and a working call: Quickstart (choose the Java tab).
  2. See everything a token can allow: Access tokens.
  3. Set up the app side: Next.js, React, React Native, or Flutter.