I’m trying to use the official LiveKit Agents Runway plugin (livekit-plugins-runway v1.5.6) to start a realtime avatar session. The plugin sends a livekit key in the request body to POST /v1/realtime_sessions, but the API returns:
{"error":"Unrecognized key: \"livekit\"","docUrl":"https://docs.dev.runwayml.com/api"}
Request details:
Endpoint: POST https://api.dev.runwayml.com/v1/realtime_sessions
X-Runway-Version: 2024-11-06
Avatar ID: d79f7323-c15e-4452-83c4-cfaea352550d (created via the Developer Portal, visible in my dashboard)
Organization: vflip
The request body the plugin sends:
{
"model": "gwm1_avatars",
"avatar": {"type": "custom", "avatarId": "..."},
"livekit": {
"url": "wss://..livekit.cloud",
"token": "...",
"roomName": "..."
}
}
This matches the official LiveKit plugin source code on GitHub (livekit/agents, merged April 6, 2026) and the LiveKit docs at https://docs.livekit.io/agents/models/avatar/plugins/runway.md.
I haven’t tried Runway, but just to confirm, have you specified the RUNWAYML_API_SECRET in your .env file (or agent secrets) ? Runway Characters integration guide | LiveKit Documentation . I would interpret that error code to mean that this secret was not recognized for some reason.
thanks for getting back to me! i set up a function to call the runway api directly, with my RUNWAYML_API_SECRET and it worked fine and the video rendered:
// ── PARKED: Direct Runway API workaround ──────────────────────────────────
// The code below bypasses the LiveKit agent and calls Runway's realtime
// session API directly. It is kept here for reference while we wait for
// the livekit-plugins-runway plugin compatibility fix.
//
// const RUNWAY_API_URL = "https://api.dev.runwayml.com";
// const RUNWAY_API_VERSION = "2024-11-06";
//
// async function createRunwaySession(
// avatar: AvatarRow
// ): Promise<{ serverUrl: string; token: string; roomName: string }> {
// const apiKey = process.env.RUNWAYML_API_SECRET;
// if (!apiKey) throw new Error("RUNWAYML_API_SECRET env var missing");
//
// const headers = {
// Authorization: `Bearer ${apiKey}`,
// "X-Runway-Version": RUNWAY_API_VERSION,
// "Content-Type": "application/json",
// };
//
// // 1. Create session with avatar personality
// const createRes = await fetch(`${RUNWAY_API_URL}/v1/realtime_sessions`, {
// method: "POST",
// headers,
// body: JSON.stringify({
// model: "gwm1_avatars",
// avatar: { type: "custom", avatarId: avatar.runway_avatar_id },
// personality: avatar.prompt || undefined,
// startScript:
// avatar.name
// ? `Hi! I'm ${avatar.name}. Nice to meet you!`
// : undefined,
// }),
// });
//
// if (!createRes.ok) {
// const text = await createRes.text();
// throw new Error(`Runway create session failed (${createRes.status}): ${text}`);
// }
//
// const { id: sessionId } = (await createRes.json()) as { id: string };
// console.log(`[runway] Created session ${sessionId}`);
//
// // 2. Poll until READY (max ~60s)
// let sessionKey: string | undefined;
// for (let i = 0; i < 60; i++) {
// await new Promise((r) => setTimeout(r, 1000));
// const pollRes = await fetch(
// `${RUNWAY_API_URL}/v1/realtime_sessions/${sessionId}`,
// { headers }
// );
// const session = (await pollRes.json()) as {
// status: string;
// sessionKey?: string;
// failure?: string;
// };
//
// if (session.status === "READY") {
// sessionKey = session.sessionKey;
// break;
// }
// if (session.status === "FAILED") {
// throw new Error(`Runway session failed: ${session.failure}`);
// }
// }
//
// if (!sessionKey) throw new Error("Runway session timed out waiting for READY");
//
// // 3. Consume to get LiveKit credentials
// const consumeRes = await fetch(
// `${RUNWAY_API_URL}/v1/realtime_sessions/${sessionId}/consume`,
// {
// method: "POST",
// headers: {
// Authorization: `Bearer ${sessionKey}`,
// "X-Runway-Version": RUNWAY_API_VERSION,
// "Content-Type": "application/json",
// },
// body: JSON.stringify({}),
// }
// );
//
// if (!consumeRes.ok) {
// const text = await consumeRes.text();
// throw new Error(`Runway consume failed (${consumeRes.status}): ${text}`);
// }
//
// const creds = (await consumeRes.json()) as {
// url: string;
// token: string;
// roomName: string;
// };
// console.log(`[runway] Got credentials for room ${creds.roomName}`);
// return { serverUrl: creds.url, token: creds.token, roomName: creds.roomName };
// }
//
// Usage in resolver (replace the LiveKit room creation + agent dispatch):
// const creds = await createRunwaySession(avatar);
// return { serverUrl: creds.serverUrl, roomName: creds.roomName, participantToken: creds.token, ... };
// ──────────────────────────────────────────────────────────────────────────
this would lead me to believe that the dev who set up the plugin didnt update the version or something? not sure