java springboot: generate livekit token which shou...
# ask-ai
r
java springboot: generate livekit token which should also have our custom agentName attached with it.
t
Thanks for reaching out, Gaurav. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I can help with that. Quick clarification so I give the right code: • Do you want
agentName
to be stored on the participant token (so the client can read it) — e.g., in
metadata
or
attributes
— OR do you want this token to trigger an automatic agent dispatch when the participant joins (room-level
agent_name
)? • Which SDK/dependency are you using to generate tokens in your Spring Boot app (plain JWT library, LiveKit server-sdk-kotlin, or something else)? • Please paste the current token-generation code snippet you have (or share the relevant Maven/Gradle dependency). Once you tell me the answers I’ll provide a concise Java example you can drop into your Spring Boot service. Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | server-sdk-go | livekit/livekit_composite | DeepWiki
r
want this token to trigger an automatic agent dispatch when the participant joins (room-level
agent_name
)
t
Thanks — since you want the token to trigger explicit agent dispatch when the participant joins, here’s a concise Java Spring Boot example using the LiveKit JVM server SDK (server-sdk-kotlin) that embeds a RoomConfiguration with
agent_name
into the token. 1) Add the SDK dependency (Maven):
Copy code
<dependency>
  <groupId>io.livekit</groupId>
  <artifactId>server-sdk-kotlin</artifactId>
  <version>1.4.0</version>
</dependency>
2) Token-generation method (Java):
Copy code
import io.livekit.server.AccessToken;
import io.livekit.server.RoomConfiguration;
import io.livekit.server.RoomAgentDispatch;
import io.livekit.server.VideoGrant;
import java.util.Optional;

public String generateTokenWithAgentDispatch(
    String apiKey,
    String apiSecret,
    String identity,
    String roomName,
    String agentName,
    String metadataJson // optional JSON string passed to the agent as job metadata
) {
    RoomAgentDispatch agentDispatch = RoomAgentDispatch.newBuilder()
        .setAgentName(agentName)
        .setMetadata(Optional.ofNullable(metadataJson).orElse(""))
        .build();

    RoomConfiguration roomConfig = RoomConfiguration.newBuilder()
        .addAgents(agentDispatch)
        .build();

    AccessToken token = new AccessToken(apiKey, apiSecret)
        .setIdentity(identity)
        .setName("User")
        .addGrants(new VideoGrant.Builder()
            .roomJoin(true)
            .room(roomName)
            .build())
        .setRoomConfig(roomConfig);

    return token.toJwt();
}
Notes: • The
agentName
in
RoomAgentDispatch
causes that specific agent to be explicitly dispatched to the room when the token is used. •
metadataJson
is optional and becomes the agent job metadata (accessible to the agent). • Automatic dispatch (assigning an agent to all rooms) is different from explicit dispatch via token; this code performs explicit dispatch. If you’d like, share your current token code or your preferred JSON structure for
metadataJson
and I’ll adapt the snippet to match. Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docs
r
tell dependency with version of livekit for this
t
Thanks — use the LiveKit JVM server SDK. A commonly used recent version is 1.4.0, but please check Maven for the latest release. Maven:
Copy code
<dependency>
  <groupId>io.livekit</groupId>
  <artifactId>server-sdk-kotlin</artifactId>
  <version>1.4.0</version>
</dependency>
Gradle (Kotlin DSL):
Copy code
implementation("io.livekit:server-sdk-kotlin:1.4.0")
Would you like me to update the token-generation snippet to exactly match your build (Maven or Gradle) or your current project setup? Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki
r
I am not using Kotlin/Gradle builds. am using Maven
t
Thanks — since you use Maven, add this dependency to your pom.xml:
Copy code
<dependency>
  <groupId>io.livekit</groupId>
  <artifactId>server-sdk-kotlin</artifactId>
  <version>1.4.0</version>
</dependency>
After Maven refresh, you can use the Java token-generation snippet I provided earlier (it works with this JVM SDK). Would you like the full pom.xml snippet or a ready-to-drop Spring Boot service method that generates the token?
r
ERROR] Failed to execute goal on project jchat-manager: Could not resolve dependencies for project com.oneassistjchat managerjar:0.0.1-SNAPSHOT [ERROR] dependency: io.livekitserver sdk kotlinjar:1.4.0 (compile) [ERROR] io.livekitserver sdk kotlinjar:1.4.0 was not found in https://repo.spring.io/milestone during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of spring-milestones has elapsed or updates are forced [ERROR] io.livekitserver sdk kotlinjar:1.4.0 was not found in https://releases.aspose.com/java/repo/ during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of AsposeJavaAPI has elapsed or updates are forced [ERROR] io.livekitserver sdk kotlinjar:1.4.0 was not found in https://repo.maven.apache.org/maven2 during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of central has elapsed or updates are forced
t
Thanks — I see Maven cannot find
io.livekit:server-sdk-kotlin:1.4.0
. That artifact is not published to the public Maven repositories you have configured, so Maven will fail to resolve it. You have three practical options — pick one and I’ll give the exact steps: • Build & install the JVM SDK locally a. Clone the SDK repo:
git clone <https://github.com/livekit/server-sdk-kotlin.git>
b. From the repo root run:
./gradlew publishToMavenLocal
c. Keep the same dependency in your pom; Maven will resolve it from your local Maven repo. • Use a different supported server SDK that is available publicly (immediate unblock) ◦ For example, generate tokens with the Node or Go server SDK (both have published packages). I can provide a minimal Node/Go token-generator you can run as a small service. • Verify alternate artifact coordinates or a published version ◦ I can search the exact groupId/artifactId/version if you want me to confirm the correct Maven coordinates. If the SDK was published under a different coordinate or version, I’ll provide the exact pom entry. Which option would you like? If you choose "build & install locally," tell me whether you have Gradle installed or want the exact commands to run. If you prefer an alternative SDK, tell me which language (Node or Go). Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki
r
I was using this dependency to generate token that doesn't use agent name:
Copy code
<dependency>
    <groupId>io.livekit</groupId>
    <artifactId>livekit-server</artifactId>
    <version>0.10.1</version>
</dependency>
t
Thanks — I checked your current dependency. The
io.livekit:livekit-server:0.10.1
artifact does NOT support embedding a room-level
agent_name
(agent dispatch) in tokens. You have three practical paths: 1) Build & use the JVM server SDK locally (closest to your Java flow) • Clone the server SDK repo:
git clone <https://github.com/livekit/server-sdk-kotlin.git>
• From the repo root run:
./gradlew publishToMavenLocal
• Add the locally published dependency to your pom (use the version printed by the build):
Copy code
<dependency>
  <groupId>io.livekit</groupId>
  <artifactId>server-sdk-kotlin</artifactId>
  <version>{version-from-build}</version>
</dependency>
I can give exact Gradle commands and the expected version string if you want. 2) Keep using Maven + your Spring Boot app but generate dispatching tokens from another service • Run a small token-generator service using a published SDK (Node or Go) that supports room config/agent dispatch. • Your Spring app calls that internal service to get tokens. I can provide a minimal Node or Go example. 3) Trigger dispatch via the Agent Dispatch API instead of token claims • After creating/joining the room, call the agent-dispatch API to ask a named agent to join (requires a server SDK or direct HTTP call to the LiveKit control API). I can show the API request pattern. Which option do you want to pursue? If you choose 1, tell me whether you can run Gradle locally and I’ll give exact commands and the pom snippet to use. If you prefer 2, tell me Node or Go and I’ll provide a ready token-generator. Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | server-sdk-js | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docs
r
is an advanced version present for io.livekitlivekit server0.10.1 ? which gives agent name option and allows it to auto join
t
Short answer: No — there is no newer public Maven version of io.livekit:livekit-server that adds token-embedded agent dispatch (agent_name auto-join). That artifact version 0.10.1 does not support embedding RoomConfiguration/RoomAgentDispatch in tokens. What you can do next (pick one) — I can provide step-by-step for any option: • Build & use the JVM server SDK locally (closest to Java/Spring) a. Clone the server SDK repo:
git clone <https://github.com/livekit/server-sdk-kotlin.git>
b. From the repo root run:
./gradlew publishToMavenLocal
c. Add the published artifact to your pom (use the version printed by the build). I can give exact Gradle commands and the pom snippet. • Use the Agent Dispatch API (no SDK build) ◦ After room creation or participant join, call the agent-dispatch endpoint to tell a named agent to join. This works with any HTTP client from your Spring app. I can provide the exact HTTP request/Java snippet. • Run a small token-generator service using a published SDK (Node or Go) ◦ Create a tiny service (Node or Go) that returns tokens with RoomConfiguration/agents and call it from your Spring app. I can provide a ready Node or Go implementation. Which option do you want to pursue? If you choose "build & use SDK locally," confirm you can run Gradle locally and I’ll give exact commands and pom entries. If you choose API or Node/Go, tell me which and I’ll send the code. Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docs
r
instead of agent name, can we make the agent join certain types of rooms only?
t
Yes — you can make an agent join only certain types of rooms without relying solely on agent name. Here are practical ways to do that; pick the one that fits your architecture and I’ll provide code for it. • Dispatch rules / room naming conventions Use room name patterns (prefixes/suffixes) and configure dispatch rules so only rooms matching that pattern get an agent. Example: dispatch only for rooms starting with
support-
or
sales-
. • Token-based conditional dispatch When you generate a token, embed a RoomConfiguration with agent dispatch only for rooms of a certain type. Create tokens for “support” rooms that include the agent dispatch, and omit it for other rooms. • Programmatic dispatch via API After a room is created or a participant joins, call the Agent Dispatch API from your backend to dispatch an agent only if the room meets your criteria (name, metadata, user role, etc.). • Agent-side filtering (early exit) Let the agent receive dispatches but immediately check room attributes (name, metadata) in its entrypoint and exit if the room doesn’t match. This is simple and keeps dispatch logic centralized in the agent. Which approach do you prefer (server-side token generation, API-driven dispatch, naming-rule dispatch, or agent-side filtering)? Tell me your current flow (how rooms are created/named and where tokens are generated) and I’ll give a focused example for that option. Sources: Agents telephony integration | LiveKit docs | Agent dispatch | LiveKit docs | server-sdk-go | livekit/livekit_composite | DeepWiki
r
• Dispatch rules / room naming conventions
t
Great — focusing on dispatch rules with room naming conventions is a good, simple approach. Below are clear, practical options and examples so agents only join rooms matching a naming pattern. 1) Server-side: embed agent dispatch in tokens only for matching room names • When your backend issues a token, check the room name. If it matches your pattern (e.g., starts with
support-
), include a RoomConfiguration with the agent dispatch; otherwise skip it. • This avoids extra API calls and only dispatches when the token is created for that room. Java pseudocode (use your token generator):
Copy code
if (roomName.startsWith("support-")) {
  RoomAgentDispatch agent = RoomAgentDispatch.newBuilder()
      .setAgentName("support-agent")
      .setMetadata("{\"source\":\"token\"}")
      .build();
  RoomConfiguration cfg = RoomConfiguration.newBuilder().addAgents(agent).build();
  token.setRoomConfig(cfg);
}
(Use your server SDK or the approach you can build locally — same conditional logic applies.) 2) Central dispatch rules (if using SIP/dispatch configuration) • Configure dispatch rules that match room prefixes and attach roomConfig for those rules. • Example rule (conceptual JSON):
Copy code
{
  "dispatch_rule": {
    "rule": {
      "roomPrefix": "support-"
    },
    "roomConfig": {
      "agents": [
        {
          "agentName": "support-agent",
          "metadata": "{\"role\":\"support\"}"
        }
      ]
    }
  }
}
• This makes any room created with
support-...
automatically receive the configured agent. 3) API-driven dispatch after room creation • If you prefer not to modify tokens, have your backend evaluate new rooms and call the Agent Dispatch API when the room name matches:
Copy code
if (roomName.startsWith("support-")) {
  // HTTP or SDK call to agent dispatch endpoint:
  // { "room": roomName, "agent_name": "support-agent", "metadata": "..." }
}
• Works well if rooms are created separately from token issuance. 4) Agent-side defensive check (recommended as a safety net) • Even with rules above, have the agent check room name on startup and exit if it doesn’t match expected patterns:
Copy code
if not ctx.room.name.startswith("support-"):
    await ctx.shutdown()
• Prevents accidental joins if dispatch rules are misapplied. Which of these do you want to implement now? • I can give a concrete Java snippet for your token generator (Maven/Spring) that conditionally sets RoomConfiguration, • or provide the exact HTTP request body to call Agent Dispatch from Java, • or an example dispatch-rule JSON you can use where you configure dispatch rules.
r
I want to keep using livekit-server dependency. can i use option 1?
t
Thanks — quick acknowledgement: you want to keep using your existing
io.livekit:livekit-server:0.10.1
dependency and implement Option 1 (conditionally embed agent dispatch in tokens by room name). Short answer: You cannot do Option 1 with
io.livekit:livekit-server:0.10.1
. That artifact does not provide the RoomConfiguration / RoomAgentDispatch features required to embed agent dispatch into tokens. What you can do next (pick one) — I can provide exact steps/snippets for any option: • Build & use the JVM server SDK locally (enables Option 1) ◦ Clone the server SDK:
git clone <https://github.com/livekit/server-sdk-kotlin.git>
◦ From the repo root run:
./gradlew publishToMavenLocal
◦ Add the published
server-sdk-kotlin
dependency to your pom and use the token code that conditionally sets
RoomConfiguration
when roomName matches your pattern. ◦ I can provide the precise Gradle commands, pom entry, and a ready-to-drop Java method that does the conditional token generation. • Keep
livekit-server
and use API-driven dispatch instead (works with your current dependency) ◦ After room creation or participant join, call the Agent Dispatch API from your Spring app when roomName matches (I can give the exact Java HTTP request body/headers). ◦ This achieves the same outcome (agent only joins support-* rooms) without changing dependencies. • Add a small helper service using a published SDK (Node/Go) to create tokens with room configs ◦ Your Spring app calls this service for tokens when roomName matches; otherwise it issues regular tokens. • As a safety net, have agents check room name on startup and exit if it doesn't match (I can provide the agent-side snippet). Which path do you want to take? If you want Option 1, confirm you can run Gradle locally and I’ll give the exact commands, pom snippet, and full Java token-generator method that conditionally embeds the agent dispatch. If you prefer to avoid changing dependencies, I’ll provide the API-based dispatch Java snippet.
r
◦ After room creation or participant join, call the Agent Dispatch API from your Spring app when roomName matches (I can give the exact Java HTTP request body/headers). how does this apply or when does this apply? after the client joins the room?
t
Good question — calling the Agent Dispatch API is something you do from your server after you confirm a room exists or after a participant joins the room. Both timings are valid; pick based on your flow: • After room creation: Call immediately when your backend creates the room (or when you detect room creation). Useful if you want the agent to be present before any participant interaction. • After participant join: Call right after the first (or any) participant successfully joins. Useful if you only want agents in rooms that actually have participants. Typical workflow (participant-join-triggered): 1. Client requests a token / joins a room via your backend. 2. Your backend issues the token and returns it to the client. 3. When the client successfully joins (or your server receives a webhook/event that someone joined), your backend checks room name. 4. If roomName matches (e.g., startsWith "support-"), call the Agent Dispatch API to dispatch the agent to that room. Example Java HTTP request (use your server API key/secret to authorize — use the same auth you use for LiveKit control operations). Replace placeholders. Request body (JSON): { "room": "support-123", "agent_name": "support-agent", "metadata": "{\"source\":\"post-join\",\"userId\":\"u123\"}" } Java example using HttpClient (Java 11+):
Copy code
String dispatchUrl = "https://<your-livekit-control-endpoint>/v1/agent/dispatch";
String body = """
{
  "room":"support-123",
  "agent_name":"support-agent",
  "metadata":"{\"source\":\"post-join\",\"userId\":\"u123\"}"
}
""";

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create(dispatchUrl))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + YOUR_SERVER_JWT) // use server auth token
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// check resp.statusCode() for success
How to trigger this in your app: • If you control token issuance: call dispatch right after issuing a token for a matching room. • If you want to ensure a join happened: use LiveKit webhooks (participant_joined) or server SDK events to detect joins, then call dispatch. • If you only need the agent when the first participant arrives, call dispatch on the first join event and ignore subsequent joins. Would you like the exact HTTP endpoint and auth pattern for your LiveKit deployment (self-hosted vs cloud) and a Spring RestTemplate example wired into a join webhook handler?
r
can we do this immediately after returning token?
t
Yes — you can call the Agent Dispatch API immediately after returning the token. A few practical notes so it works reliably: • Timing: the room may not exist yet the moment you hand back the token. Calling dispatch right away is fine — LiveKit will dispatch the agent and the agent will join once the room is created. If you need the agent to be present before the first participant connects, dispatch immediately after token issuance. If you only want agents after a confirmed join, trigger on a webhook/event instead. • Idempotency: make your dispatch call idempotent (ignore duplicate dispatches for the same room) so retries or multiple token requests don’t spawn duplicates. • Which token to return: return the normal participant token; agent dispatch is a separate control API call (it does not require embedding anything into the participant token). Example Java (Java 11+ HttpClient) — call dispatch right after you generate/return the token:
Copy code
// 1) generate token and return it to client (your existing code)
// String jwt = generateToken(...);
// return jwt to client

// 2) then call dispatch
String dispatchUrl = "https://<your-livekit-control-endpoint>/v1/agent/dispatch";
String body = String.format("""
  {
    "room":"%s",
    "agent_name":"support-agent",
    "metadata":"%s"
  }
  """, roomName, "{\"source\":\"post-token\",\"userId\":\"u123\"}");

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create(dispatchUrl))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + YOUR_SERVER_JWT)
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// check resp.statusCode() and handle errors/retries as needed
Recommendations: • If you want agent present as soon as possible: dispatch immediately after returning token. • If you must ensure a participant actually joined: listen for
participant_joined
webhook/event and dispatch on the first join. • Add retry/backoff and idempotency checks for robustness. Do you want a Spring RestTemplate or WebClient version of the dispatch call and a short example of wiring it into your token-issuance endpoint?
r
where to listen for
participant_joined
webhook/event
t
You can listen for participant_joined events by configuring LiveKit to send webhooks to your server, then implementing an HTTP endpoint that validates the webhook and triggers your agent-dispatch logic. Below are concise steps and a Spring Boot example. 1) Configure LiveKit webhooks • For LiveKit Cloud: set your project's webhook URL in the LiveKit console (or use the control-plane API). • For self-hosted LiveKit: set `webhook`/`webhook_auth` in your LiveKit config to point to your endpoint. 2) Implement a webhook endpoint in Spring Boot • Expose a public HTTPS POST endpoint (e.g., /webhook/livekit). • Verify authenticity (HMAC or bearer header depending on your LiveKit setup). Reject invalid requests. • Parse the JSON payload and act on
event
or
type
==
participant_joined
(or check payload fields). Minimal Spring Boot controller (Java 11+):
Copy code
@RestController
@RequestMapping("/webhook/livekit")
public class LivekitWebhookController {

  private final String expectedSecret = "YOUR_WEBHOOK_SECRET"; // config

  @PostMapping
  public ResponseEntity<String> handleWebhook(@RequestHeader Map<String,String> headers,
                                              @RequestBody String body) {
    // 1) verify signature (example HMAC-SHA256 header "X-Signature")
    String sig = headers.getOrDefault("X-Signature", "");
    if (!verifyHmacSha256(body, sig, expectedSecret)) {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("invalid signature");
    }

    // 2) parse event
    JSONObject json = new JSONObject(body);
    String event = json.optString("event", json.optString("type", ""));
    if ("participant_joined".equals(event) || "participant.joined".equals(event)) {
      String room = json.getJSONObject("participant").optString("room");
      String participantSid = json.getJSONObject("participant").optString("sid");
      // 3) conditional dispatch
      if (room != null && room.startsWith("support-")) {
        dispatchAgentToRoom(room, "support-agent", "{\"source\":\"webhook\",\"participantSid\":\"" + participantSid + "\"}");
      }
    }

    return ResponseEntity.ok("ok");
  }

  private boolean verifyHmacSha256(String payload, String signature, String secret) {
    // implement HMAC-SHA256 of payload with secret and compare to signature
    return true;
  }

  private void dispatchAgentToRoom(String room, String agentName, String metadata) {
    // call Agent Dispatch API (same pattern as earlier example)
  }
}
3) Dispatch call • From the webhook handler call your Agent Dispatch endpoint (HTTP POST) with JSON: { "room": "support-123", "agent_name":"support-agent", "metadata":"{...}" } • Authenticate using your server credentials (Bearer server JWT or other configured auth). 4) Reliability & security • Validate signatures to prevent spoofing. • Make the webhook handler idempotent (ignore duplicate participant_joined events). • Acknowledge quickly: return 200 OK after queuing any heavier work (dispatch call can be async). • Log events and failures; add retry/backoff for dispatch calls. Would you like a copy of the exact HMAC verification code and a complete Dispatch HTTP client (RestTemplate/WebClient) to drop into your Spring Boot app?
r
so if I am already running the agent in another python app which which has session_handlers and room_handlers like @session.on("user_input_transcribed") and @room.on("track_subscribed"), how would adding "@RestController @RequestMapping("/webhook/livekit") public class LivekitWebhookController" in java affect that? or would it not affect it at all?
t
They will not affect each other. • Webhooks (your Spring Boot
@RestController
at /webhook/livekit) are HTTP callbacks from the LiveKit server for room-level events (e.g., participant_joined). They run on your backend and are used for orchestration, logging, dispatching agents, etc. • Agent event handlers in your Python app (
@session.on(...)
,
@room.on(...)
) run inside the agent process and handle in-room, real-time events (media, transcriptions, track subscriptions). How they work together: • When a participant joins, LiveKit can both send a webhook to your Java endpoint and deliver in-room events to your Python agent. Those are independent paths — adding the Java webhook endpoint will not interfere with the Python agent’s handlers. • Use webhooks for orchestration (e.g., call Agent Dispatch API when roomName matches). Use the Python agent for real-time interaction logic (transcription, responses). • Add idempotency/coordination (DB flags or short-lived locks) if both systems might perform the same action (to avoid duplicates). If you want, I can: • Provide a Spring WebClient/RestTemplate snippet to call the Agent Dispatch API from your webhook handler, or • Show a simple DB/lock pattern to coordinate actions between the webhook service and the Python agent. Which would you prefer? Sources: server-sdk-ruby | livekit/livekit_composite | DeepWiki | examples/survey | livekit/livekit_composite | DeepWiki | Agent session | LiveKit docs | server-sdk-js | livekit/livekit_composite | DeepWiki
r
"the room may not exist yet the moment you hand back the token. Calling dispatch right away is fine — LiveKit will dispatch the agent and the agent will join once the room is created. If you need the agent to be present before the first participant connects, dispatch immediately after token issuance." what is the difference? "Calling dispatch right away " and "dispatch immediately after token issuance."
t
Good question — the two phrases are the same action described slightly differently; the difference is in implied timing relative to other events: • "Calling dispatch right away" — means you call the Agent Dispatch API immediately from your server as soon as you decide the room should get an agent. That could be: ◦ right after you create the room, or ◦ right after you return a token to the client, or ◦ triggered by some other immediate server-side event. • "Dispatch immediately after token issuance" — is the same immediate call, but emphasizes that you perform the dispatch call directly after you generate/return the participant token. Practical difference to consider: • If you dispatch immediately after token issuance, you typically do this in the same request flow that issues the token (synchronous or fire-and-forget async). The room may not yet exist, but LiveKit will queue/handle the dispatch so the agent will join when the room is created. • If you "call dispatch right away" but in a different moment (for example, after you receive a webhook that someone joined), you’re waiting for a different trigger (room created or participant joined) before calling dispatch. Which to pick: • Dispatch after token issuance: good if you want the agent present as soon as the room is created or participant connects; minimal latency if you dispatch immediately. • Dispatch after participant join (webhook/event): good if you only want agents for rooms that actually get participants and want to avoid unnecessary agent starts. Implementation tips: • Make the dispatch call idempotent and async-safe (so duplicate token requests or retries don’t spawn duplicates). • If you dispatch before the room exists, it’s fine — LiveKit will handle the agent join when the room appears. • If you need the agent to be in-room before the very first media arrives, dispatch in the token-issuance flow. If you want, I’ll provide a concise Java example showing dispatch done synchronously right after token generation (in the same endpoint) and an alternative that triggers dispatch from a webhook on first participant join.
r
can I call dispatch before I actually return the token to the client.
t
Yes — you can call the Agent Dispatch API before returning the token to the client. That is a valid flow and often used when you want the agent to start joining as soon as the room is created or the client connects. Key points: • Rooms may not exist yet when you dispatch. LiveKit will accept the dispatch and the agent will join once the room is created/available. • Doing dispatch before returning the token can reduce end-to-end latency (agent starts joining sooner). • Make the dispatch call idempotent and non-blocking (or use async with retry) so token issuance isn't delayed by transient dispatch failures. • If you must guarantee the agent is already in the room before the participant connects, you can wait for a successful dispatch response and/or poll room state — but often dispatch + immediate token return is sufficient. Concise Java pattern (same HTTP request flow):
Copy code
// 1) generate participant token
String token = generateParticipantToken(...);

// 2) call Agent Dispatch API (async or fire-and-forget is recommended)
String dispatchBody = String.format("{\"room\":\"%s\",\"agent_name\":\"%s\",\"metadata\":\"%s\"}",
    roomName, "support-agent", "{\"source\":\"pre-token\"}");

HttpRequest dispatchReq = HttpRequest.newBuilder()
    .uri(URI.create(dispatchUrl))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + SERVER_JWT)
    .POST(HttpRequest.BodyPublishers.ofString(dispatchBody))
    .build();

// fire-and-forget:
HttpClient.newHttpClient().sendAsync(dispatchReq, HttpResponse.BodyHandlers.discarding())
    .whenComplete((r, ex) -> {
        if (ex != null) { /* log + retry/backoff as needed */ }
    });

// 3) return token to client immediately
return ResponseEntity.ok(Map.of("token", token));
Would you like a Spring WebClient or RestTemplate example and a recommended idempotency key pattern for the dispatch call?
r
what will be the values of dispatchUrl and SERVER_JWT
t
Good — here are the concrete values and how to obtain them. • dispatchUrl ◦ Format: https://<your-livekit-host>/twirp/livekit.AgentDispatch/CreateDispatch ◦ Examples: ▪︎ LiveKit Cloud: https://<your-project>.livekit.cloud/twirp/livekit.AgentDispatch/CreateDispatch ▪︎ Self-hosted: https://livekit.example.com/twirp/livekit.AgentDispatch/CreateDispatch • SERVER_JWT ◦ A server-auth JWT you generate using your LiveKit API Key and API Secret (not a participant token). ◦ Create it server-side and sign with your API Secret. Example (Java pseudocode using AccessToken helper):
Copy code
AccessToken serverToken = new AccessToken(API_KEY, API_SECRET);
serverToken.setIdentity("server");
serverToken.setValidFor(Duration.ofHours(1));
String SERVER_JWT = serverToken.toJwt();
• Using them together (Java 11+ HttpClient example):
Copy code
String dispatchUrl = "https://<your-livekit-host>/twirp/livekit.AgentDispatch/CreateDispatch";
String body = String.format("{\"room\":\"%s\",\"agent_name\":\"%s\",\"metadata\":\"%s\"}",
    roomName, agentName, metadataJson);

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create(dispatchUrl))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + SERVER_JWT)
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpClient.newHttpClient()
    .sendAsync(req, HttpResponse.BodyHandlers.ofString())
    .thenAccept(resp -> {
        // check resp.statusCode()
    });
Notes: • Generate SERVER_JWT per your server process; short TTL (e.g., 1 hour) is recommended. • The AgentDispatch endpoint accepts the request even if the room doesn't yet exist; the agent will join once the room is available. • Make dispatch calls idempotent and non-blocking (async + retry/backoff). Tell me your deployment (LiveKit Cloud or self-hosted) and I’ll give the exact dispatchUrl for your setup and a ready Spring WebClient snippet. Sources: Server-side | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docs | Agents Framework | livekit/livekit_composite | DeepWiki
r
okay give modified code from the following one:
Copy code
package com.oneassist.jcm.proxy.livekit;

import com.oneassist.jcm.api.dto.response.VoiceAccessTokenResponseDto;
import io.livekit.server.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;

import java.security.SecureRandom;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Spring Boot Application and REST Controller for generating LiveKit Tokens.
 * To run:
 * 1. Ensure the required Maven dependencies (livekit-server-sdk, spring-boot-starter-web) are included.
 * 2. Set environment variables: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET.
 * 3. Run the main method: mvn spring-boot:run
 */
@Component
@Slf4j
public class LivekitProxy {

    @Value("${livekit_api_key}")
    private String apiKey;

    @Value("${livekit_api_secret}")
    private String apiSecret;

    @Value("${livekit_url}")
    private String livekitUrl;

    private static final Set<String> VALID_LANGUAGES = Set.of(
            "hi-IN", "en-IN", "bn-IN", "gu-IN", "kn-IN", "ml-IN", "mr-IN", "or-IN", "pa-IN", "ta-IN", "te-IN"
    );

    private static final String DEFAULT_LANGUAGE = "hi-IN";
    private static final String DEFAULT_VOICE = "anushka";

    private final SecureRandom random = new SecureRandom();


    public VoiceAccessTokenResponseDto generateLivekitToken(Integer chatId, String name, String agentType) {

        try {
            // Validate and normalize agentType
            if (agentType == null || agentType.trim().isEmpty()) {
                agentType = List.of("sarvam", "gemini")
                   .get(ThreadLocalRandom.current().nextInt(3));
            }
            agentType = agentType.toLowerCase().trim();
            
            // Validate agentType - only allow 'gemini' or 'sarvam'
            if (!agentType.equals("gemini") && !agentType.equals("sarvam")) {
                log.warn("Invalid agentType '{}' provided, defaulting to 'sarvam'", agentType);
                agentType = "sarvam";
            }

            final String language =DEFAULT_LANGUAGE;
            final String voice = DEFAULT_VOICE;

            // Include agent type prefix in room name for routing
            // LiveKit will route rooms to agents based on:
            // 1. Room name patterns (prefix matching: "gemini-" -> "gemini-agent", "sarvam-" -> "sarvam-agent")
            // 2. Agent names set in agent code: "gemini-agent" and "sarvam-agent"
            String roomBase = agentType + "-conversation-ai-" + generateRandomHex(8);

            boolean hasSuffix = false;
            for (String lang : VALID_LANGUAGES) {
                if (roomBase.contains("-" + lang) || roomBase.endsWith("-" + lang)) {
                    hasSuffix = true;
                    break;
                }
            }

            String roomName;
            if (!hasSuffix) {
                if (chatId != null && chatId!=0) {
                    roomName = String.format("%s-%s-%s-%s", roomBase, chatId, language, voice);
                } else {
                    roomName = String.format("%s-%s-%s", roomBase, language, voice);
                }
            } else {
                roomName = roomBase;
            }

            String participantName = name != null && !name.isEmpty()
                    ? name
                    : "user-" + generateRandomHex(4);

            <http://log.info|log.info>("Generating token for participant: {} in room: {} (agentType: {}, language: {}, voice: {}, chat_id: {})", participantName, roomName, agentType, language, voice, chatId);

            String agentName = agentType + "-agent";
            AccessToken token = new AccessToken(apiKey, apiSecret);
            token.setTtl(600);
            token.setIdentity(participantName);
            token.setName(participantName);

            token.addGrants(
                new RoomJoin(true),
                new Room(roomName),
                new CanPublish(true),
                new CanSubscribe(true),
                new CanPublishData(true)
            );

            String jwt = token.toJwt();
            
            <http://log.info|log.info>("Token generated for room: {} with agent_type: {} (agent_name: {})", roomName, agentType, agentName);

            // 5. Prepare response (convert URL to wss/https if needed)
            String safeUrl = livekitUrl.replace("ws://", "wss://").replace("http://", "https://");

            VoiceAccessTokenResponseDto.VoiceAccessTokenResponseDtoBuilder responseBuilder = VoiceAccessTokenResponseDto.builder()
                    .authToken(jwt)
                    .wssUrl(safeUrl)
                    .roomName(roomName)
                    .language(language)
                    .voice(voice)
                    .agentName(agentName); // Include agent_name in response

            if (chatId != null && chatId!=0) {
                responseBuilder.chatId(chatId);
            }

            VoiceAccessTokenResponseDto responseDto = responseBuilder.build();

            <http://log.info|log.info>("Token generated successfully for room: {}", roomName);
            return responseDto;

        } catch (Exception e) {
            log.error("Exception while generating token," , e);
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token: " + e.getMessage());
        }
    }

    private String generateRandomHex(int length) {
        byte[] bytes = new byte[length];
        random.nextBytes(bytes);
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
t
Acknowledged — below is a minimal, safe modification of your class that calls the Agent Dispatch API asynchronously just before returning the token. It: • Builds
dispatchUrl
from your
livekitUrl
. • Generates a server JWT (
SERVER_JWT
) using your API key/secret. • Sends an async POST to the AgentDispatch CreateDispatch endpoint with idempotency and basic retry logging. • Keeps your existing token-generation behavior and response DTO unchanged. Drop this into your project (imports included). Adjust logging, retry/backoff, and secret handling as needed.
Copy code
package com.oneassist.jcm.proxy.livekit;

import com.oneassist.jcm.api.dto.response.VoiceAccessTokenResponseDto;
import io.livekit.server.AccessToken;
import io.livekit.server.CanPublish;
import io.livekit.server.CanPublishData;
import io.livekit.server.CanSubscribe;
import io.livekit.server.Room;
import io.livekit.server.RoomJoin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.json.JSONObject;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

@Component
@Slf4j
public class LivekitProxy {

    @Value("${livekit_api_key}")
    private String apiKey;

    @Value("${livekit_api_secret}")
    private String apiSecret;

    @Value("${livekit_url}")
    private String livekitUrl;

    private static final Set<String> VALID_LANGUAGES = Set.of(
            "hi-IN", "en-IN", "bn-IN", "gu-IN", "kn-IN", "ml-IN", "mr-IN", "or-IN", "pa-IN", "ta-IN", "te-IN"
    );

    private static final String DEFAULT_LANGUAGE = "hi-IN";
    private static final String DEFAULT_VOICE = "anushka";

    private final SecureRandom random = new SecureRandom();

    private final HttpClient httpClient = HttpClient.newHttpClient();

    public VoiceAccessTokenResponseDto generateLivekitToken(Integer chatId, String name, String agentType) {

        try {
            // Validate and normalize agentType
            if (agentType == null || agentType.trim().isEmpty()) {
                agentType = List.of("sarvam", "gemini")
                   .get(ThreadLocalRandom.current().nextInt(2));
            }
            agentType = agentType.toLowerCase().trim();

            if (!agentType.equals("gemini") && !agentType.equals("sarvam")) {
                log.warn("Invalid agentType '{}' provided, defaulting to 'sarvam'", agentType);
                agentType = "sarvam";
            }

            final String language = DEFAULT_LANGUAGE;
            final String voice = DEFAULT_VOICE;

            String roomBase = agentType + "-conversation-ai-" + generateRandomHex(8);

            boolean hasSuffix = false;
            for (String lang : VALID_LANGUAGES) {
                if (roomBase.contains("-" + lang) || roomBase.endsWith("-" + lang)) {
                    hasSuffix = true;
                    break;
                }
            }

            String roomName;
            if (!hasSuffix) {
                if (chatId != null && chatId != 0) {
                    roomName = String.format("%s-%s-%s-%s", roomBase, chatId, language, voice);
                } else {
                    roomName = String.format("%s-%s-%s", roomBase, language, voice);
                }
            } else {
                roomName = roomBase;
            }

            String participantName = name != null && !name.isEmpty()
                    ? name
                    : "user-" + generateRandomHex(4);

            <http://log.info|log.info>("Generating token for participant: {} in room: {} (agentType: {}, language: {}, voice: {}, chat_id: {})",
                    participantName, roomName, agentType, language, voice, chatId);

            String agentName = agentType + "-agent";
            AccessToken token = new AccessToken(apiKey, apiSecret);
            token.setTtl(600);
            token.setIdentity(participantName);
            token.setName(participantName);

            token.addGrants(
                new RoomJoin(true),
                new Room(roomName),
                new CanPublish(true),
                new CanSubscribe(true),
                new CanPublishData(true)
            );

            String jwt = token.toJwt();

            <http://log.info|log.info>("Token generated for room: {} with agent_type: {} (agent_name: {})", roomName, agentType, agentName);

            // Prepare response
            String safeUrl = livekitUrl.replace("ws://", "wss://").replace("http://", "https://");

            VoiceAccessTokenResponseDto.VoiceAccessTokenResponseDtoBuilder responseBuilder = VoiceAccessTokenResponseDto.builder()
                    .authToken(jwt)
                    .wssUrl(safeUrl)
                    .roomName(roomName)
                    .language(language)
                    .voice(voice)
                    .agentName(agentName);

            if (chatId != null && chatId!=0) {
                responseBuilder.chatId(chatId);
            }

            VoiceAccessTokenResponseDto responseDto = responseBuilder.build();

            // --- NEW: dispatch agent BEFORE returning token (async fire-and-forget) ---
            try {
                String dispatchUrl = buildDispatchUrl(livekitUrl);
                String serverJwt = buildServerJwt();
                String metadata = new JSONObject().put("source", "pre-token").put("chatId", chatId == null ? JSONObject.NULL : chatId).toString();

                JSONObject body = new JSONObject();
                body.put("room", roomName);
                body.put("agent_name", agentName);
                body.put("metadata", metadata);
                // Optional: include an idempotency key to avoid duplicate dispatches
                String idempotencyKey = "dispatch-" + roomName;

                HttpRequest req = HttpRequest.newBuilder()
                        .uri(URI.create(dispatchUrl))
                        .timeout(Duration.ofSeconds(5))
                        .header("Content-Type", "application/json")
                        .header("Authorization", "Bearer " + serverJwt)
                        .header("Idempotency-Key", idempotencyKey)
                        .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
                        .build();

                // fire-and-forget; log result or schedule retry on failure
                httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString())
                        .thenAccept(resp -> {
                            int status = resp.statusCode();
                            if (status >= 200 && status < 300) {
                                log.info("Agent dispatch request succeeded for room {} (agent={})", roomName, agentName);
                            } else {
                                log.warn("Agent dispatch failed for room {} (status={}, body={})", roomName, status, resp.body());
                                // optional: enqueue retry
                            }
                        })
                        .exceptionally(ex -> {
                            log.error("Agent dispatch request error for room {}", roomName, ex);
                            return null;
                        });

            } catch (Exception ex) {
                log.error("Failed to call agent dispatch (continuing to return token) for room {}", roomName, ex);
                // do NOT fail token issuance if dispatch fails here
            }

            <http://log.info|log.info>("Token generated successfully for room: {}", roomName);
            return responseDto;

        } catch (Exception e) {
            log.error("Exception while generating token,", e);
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token: " + e.getMessage());
        }
    }

    private String buildDispatchUrl(String livekitUrl) {
        // Normalize livekitUrl and construct twirp endpoint
        String normalized = livekitUrl.replace("ws://", "https://").replace("wss://", "https://").replace("http://", "https://");
        // remove trailing slash if any
        if (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        return normalized + "/twirp/livekit.AgentDispatch/CreateDispatch";
    }

    private String buildServerJwt() {
        // Server JWT for authenticating control API calls
        AccessToken serverToken = new AccessToken(apiKey, apiSecret);
        serverToken.setIdentity("server");
        serverToken.setTtl(3600); // 1 hour
        return serverToken.toJwt();
    }

    private String generateRandomHex(int length) {
        byte[] bytes = new byte[length];
        random.nextBytes(bytes);
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
Notes: • This keeps token issuance resilient: a dispatch failure logs but does not prevent returning the token. • The dispatch request is asynchronous; if you need to ensure the agent is in-room before the client connects, change to synchronous send and wait for success (with timeout) — but that increases latency. • Add stronger idempotency and retry/backoff for production. • Ensure your LiveKit control endpoint uses HTTPS and your apiKey/apiSecret are secured (env vars). • If your LiveKit Cloud URL uses a different domain pattern, adjust
buildDispatchUrl
accordingly.
r
the application is successfully deployed and it also showed the message "*Agent dispatch request succeeded* for room sarvam-conversation-ai-0785e0dedc565611-5846068-hi-IN-anushka (agent=sarvam-agent)" but agent still did not connect to the room.
t
Thanks — glad the dispatch request returned success. Since the agent still didn't join, here are focused troubleshooting steps to find the cause and fix it. 1) Verify agent is running and registered with the exact name • Confirm your Python agent process is up and the configured agent name matches exactly (e.g., "sarvam-agent"). • Check agent startup logs for registration messages like "registered" or "listening for dispatch". 2) Check agent logs for dispatch/job activity • Look for logs showing receipt of a dispatch job, job creation, or any errors (auth, connection, exceptions). • Search for keywords: dispatch, job, join, connect, error, exception, ctx.shutdown. 3) Confirm the dispatch job exists server-side • Use your LiveKit control/dashboard or API to list dispatch jobs (or agent dispatches) and inspect the job state. The job should be created and not in an error/failed state. 4) Ensure agent can reach LiveKit server (network/auth) • From the agent host, verify outgoing access to your LiveKit RTC/Control endpoints. • Verify the agent uses correct API key/secret and control URL if required. • Confirm TLS and DNS are correct (especially when self-hosted). 5) Confirm room name and timing • Ensure the dispatched room name exactly matches the room your participant will join. • If you dispatched before the room existed, LiveKit will join agent when the room is created — check agent logs for a delayed join attempt. • If you need the agent before participants arrive, dispatch first and wait for a successful agent join log before clients connect. 6) Agent-side filter/early-exit • Make sure the agent code does not immediately exit for rooms that don't match an internal filter (e.g., your agent might shut down if it checks room name incorrectly). • Temporarily disable early-exit logic to confirm the agent joins. 7) Inspect agent dependencies (transcription/LLM) failures • If agent crashes during initialization (missing creds, API failures), it may fail to join. Check for exceptions from dependencies (audio, transcription, LLM). 8) Reproduce manually • Manually create the same room name in LiveKit and call dispatch; watch agent logs in real time to see behavior. • Alternatively, join the room as a client and watch if the agent attempts to join. If you can share: • Agent startup and dispatch-related log snippets (redact secrets) • The exact agent name configured • Whether LiveKit is cloud or self-hosted I’ll point to the most likely root cause and provide exact fixes or additional checks (e.g., sample log patterns to look for, API calls to list dispatch jobs).
r
since this is not working, I want to adopt the webhook approach for when a participant has joined.
t
Good choice — webhooks on participant join are reliable and avoid timing/race issues. Below is a concise, practical plan plus a ready-to-drop Spring Boot webhook controller that: • verifies the webhook signature, • extracts room and participant info, • calls the Agent Dispatch API (async, idempotent), • logs and tolerates failures so your app stays resilient. 1) Configure LiveKit webhooks • LiveKit Cloud: set your project webhook URL in the LiveKit console to https://<your-app>/webhook/livekit (or use control-plane API). • Self-hosted: set
webhook
/
webhook_auth
in your LiveKit config to point to the same URL and secret. • Use a shared webhook secret (HMAC) or the LiveKit-specified header so your server can verify events. 2) Where this runs • Add the controller to your Spring Boot service that already issues tokens. It does not affect your Python agent; it simply triggers dispatch when a join happens. 3) Webhook handling behavior • On receiving participant_joined: check room name pattern (e.g., startsWith("sarvam-")). If match, call AgentDispatch CreateDispatch with { room, agent_name, metadata }. • Make dispatch idempotent (use an Idempotency-Key header or persist a dispatch flag per room) so retry/dedup is safe. • Prefer async dispatch call (fire-and-forget) and log results. If you need to guarantee the agent is present, call synchronously with timeout and retries (but this increases request latency). 4) Spring Boot controller (drop into your app) • Replace placeholders: LIVEKIT_CONTROL_URL (your livekit host), LIVEKIT_WEBHOOK_SECRET, API_KEY, API_SECRET, AGENT_NAME, and pattern check.
Copy code
@RestController
@RequestMapping("/webhook/livekit")
public class LivekitWebhookController {

    @Value("${livekit_control_url}") // e.g. https://<your-project>.livekit.cloud
    private String livekitControlUrl;

    @Value("${livekit_api_key}")
    private String apiKey;

    @Value("${livekit_api_secret}")
    private String apiSecret;

    @Value("${livekit_webhook_secret}")
    private String webhookSecret;

    @Value("${dispatch_agent_name}")
    private String agentName; // e.g. "sarvam-agent"

    private final HttpClient httpClient = HttpClient.newHttpClient();

    @PostMapping
    public ResponseEntity<String> handleWebhook(@RequestHeader Map<String,String> headers,
                                                @RequestBody String body) {
        try {
            // 1) verify signature (example header "X-LiveKit-Signature" using HMAC-SHA256)
            String sigHeader = headers.getOrDefault("x-livekit-signature", headers.getOrDefault("X-LiveKit-Signature",""));
            if (!verifyHmacSha256(body, sigHeader, webhookSecret)) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("invalid signature");
            }

            // 2) parse payload
            JSONObject json = new JSONObject(body);
            String event = json.optString("event", json.optString("type", ""));
            if (!"participant_joined".equals(event) && !"participant.joined".equals(event)) {
                return ResponseEntity.ok("ignored");
            }

            JSONObject participant = json.optJSONObject("participant");
            if (participant == null) return ResponseEntity.ok("no participant");

            String room = participant.optString("room");
            if (room == null || room.isEmpty()) return ResponseEntity.ok("no room");

            // 3) decide whether to dispatch (room naming convention)
            if (!room.startsWith("sarvam-") && !room.startsWith("support-")) {
                return ResponseEntity.ok("no dispatch needed");
            }

            // 4) build dispatch request
            String dispatchUrl = buildDispatchUrl(livekitControlUrl);
            String serverJwt = buildServerJwt(apiKey, apiSecret);
            JSONObject bodyJson = new JSONObject();
            bodyJson.put("room", room);
            bodyJson.put("agent_name", agentName);
            bodyJson.put("metadata", new JSONObject().put("trigger", "webhook").put("participantSid", participant.optString("sid")).toString());

            String idempotencyKey = "dispatch-" + room; // simple idempotency; consider DB flag

            HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(dispatchUrl))
                    .timeout(Duration.ofSeconds(5))
                    .header("Content-Type", "application/json")
                    .header("Authorization", "Bearer " + serverJwt)
                    .header("Idempotency-Key", idempotencyKey)
                    .POST(HttpRequest.BodyPublishers.ofString(bodyJson.toString()))
                    .build();

            // 5) async send; log result
            httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString())
                    .thenAccept(resp -> {
                        int status = resp.statusCode();
                        if (status >= 200 && status < 300) {
                            log.info("Agent dispatch succeeded for room {} (agent={})", room, agentName);
                        } else {
                            log.warn("Agent dispatch failed for room {} status={} body={}", room, status, resp.body());
                        }
                    })
                    .exceptionally(ex -> {
                        log.error("Agent dispatch error for room {}", room, ex);
                        return null;
                    });

            return ResponseEntity.ok("dispatch triggered");

        } catch (Exception e) {
            log.error("webhook handling error", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("error");
        }
    }

    private boolean verifyHmacSha256(String payload, String signatureHeader, String secret) {
        if (signatureHeader == null || signatureHeader.isEmpty()) return false;
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] expected = HexFormat.of().parseHex(bytesToHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8))));
            // signatureHeader may be hex or base64 depending on your LiveKit config; adapt parsing accordingly
            String expectedHex = bytesToHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
            return signatureHeader.equalsIgnoreCase(expectedHex);
        } catch (Exception ex) {
            log.error("signature verify error", ex);
            return false;
        }
    }

    private String buildDispatchUrl(String livekitUrl) {
        String normalized = livekitUrl.replace("ws://", "https://").replace("wss://", "https://").replace("http://", "https://");
        if (normalized.endsWith("/")) normalized = normalized.substring(0, normalized.length()-1);
        return normalized + "/twirp/livekit.AgentDispatch/CreateDispatch";
    }

    private String buildServerJwt(String apiKey, String apiSecret) {
        AccessToken serverToken = new AccessToken(apiKey, apiSecret);
        serverToken.setIdentity("server");
        serverToken.setTtl(3600); // 1 hour
        return serverToken.toJwt();
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b: bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }
}
5) Best practices & notes • Signature format: confirm whether LiveKit signs webhooks as hex or base64 and adapt verifyHmacSha256 accordingly. • Idempotency: use a small DB table or cache to mark dispatched rooms to avoid duplicate dispatches. • Quick response: return 200 quickly; do async dispatch to avoid webhook timeouts. • Security: restrict the webhook endpoint (IP allowlist, TLS) and keep the webhook secret and API credentials safe. • Monitoring: log received events and dispatch outcomes; attach alerting for repeated failures. If you want, I can: • adapt the verifyHmacSha256 method to match the exact header encoding your LiveKit instance uses (paste one example webhook header), or • provide a full RestTemplate / WebClient implementation instead of HttpClient, or • add example DB idempotency code using Redis or your existing DB. Which would you like next?
r
where do i get livekit_webhook_secret from
t
The webhook secret is a value you (or LiveKit) configure and use to sign webhook payloads so your server can verify requests. Where to get / set it: • LiveKit Cloud ◦ In the LiveKit project settings (webhooks or integrations), set a webhook URL and a webhook secret. The console shows or lets you create the secret. Use that secret value as
livekit_webhook_secret
in your app. • Self‑hosted LiveKit ◦ In your LiveKit server config (typically the YAML config), set
webhook.auth
/
webhook_secret
(or
webhook_auth
) to the secret you choose. Restart LiveKit. Use that same secret in your app. How to use it: • Store the secret in your environment or Spring config (e.g., application.properties): livekit_webhook_secret=your_secret_here • Verify incoming webhooks by computing HMAC-SHA256(payload, secret) and comparing with the signature header LiveKit sends (check your deployment whether the header is hex or base64 and match that format). Security tips: • Keep the secret out of code (env vars, secrets manager). • Return 401 on signature mismatch. • Optionally restrict webhook endpoint by IP, TLS, or mutual TLS. • Use idempotency to handle retries. If you tell me whether you’re on LiveKit Cloud or self-hosted, I’ll give the exact place in the console or the config snippet and an HMAC verification snippet matching LiveKit’s header encoding.