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:
<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.
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 method | Default | Notes |
|---|---|---|
apiKey(String) | required | Never read from the environment implicitly. |
baseUrl(String) | http://localhost:4100 | Required in production. A trailing / is trimmed. |
timeout(Duration) | 10 s | Per attempt. |
maxRetries(int) | 2 | Retries network errors, timeouts and 429/502/503/504. |
httpClient(HttpClient) | built-in | For 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.
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 tokenparticipants().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 deleteLive 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, terminallivqeno.live()returns the same object aslivqeno.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
BROADCASTstream, passViewerTokenOptions.acceptingPlayback()and readplayback().llhlsUrl()(orplayback().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.
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
| Setting | Behavior |
|---|---|
| Retries | Network errors, timeouts, 429, 502, 503 and 504 only, with exponential backoff from 300 ms. maxRetries defaults to 2. Other 4xx errors are never retried. |
| Timeouts | timeout(Duration), default 10 s, per attempt. |
| Lists | Chat 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
- Build a token endpoint and a working call: Quickstart (choose the Java tab).
- See everything a token can allow: Access tokens.
- Set up the app side: Next.js, React, React Native, or Flutter.