LiveKit Agents Runway plugin - Unrecognized key: \"livekit\"

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