The AI agent is joining my call but it is not responding
03:10 29 Oct 2025

The AI agent joins my call but doesn’t respond. I’m testing it using webhooks. I know that the real-time agent requires OpenAI credits, and I've already added them, but the issue is that no error message appears.

I am running the app and the webhook to start it

ngrok http --domain=nicolasa-unequivalent-temperance.ngrok-free.dev 3000

What am I missing?

src/app/api/webhook/route.ts:

import { and, ConsoleLogWriter, eq, not } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
import {
    CallEndedEvent,
    MessageNewEvent,
    CallTranscriptionReadyEvent,
    CallSessionParticipantLeftEvent,
    CallRecordingReadyEvent,
    CallSessionStartedEvent
} from "@stream-io/node-sdk";


import { db } from "@/db";
import { agents, meetings } from "@/db/schema";
import { streamVideo } from "@/lib/stream-video";
import { inngest } from "@/inngest/client";

function verifySignatureWithSDK(body: string, signature: string): boolean {
    return streamVideo.verifyWebhook(body, signature);
}


export async function POST(req: NextRequest) {
    const signature = req.headers.get("x-signature");
    const apiKey = req.headers.get("x-api-key");

    if (!signature || !apiKey) {
        return NextResponse.json(
            { error: "Missing signature or API key" },
            { status: 400 }

        );
    }

    const body = await req.text();
    if (!verifySignatureWithSDK(body, signature)) {
        return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
    }

    let payload: unknown;

    try {
        payload = JSON.parse(body) as Record;
    } catch {
        return NextResponse.json({ error: "Invalid JSON" }, { status: 400 })
    }

    const eventType = (payload as Record)?.type;

    // 1
    console.log(`[Webhook] Received event: ${eventType}`);

    if (eventType === "call.session_started") {
        const event = payload as CallSessionStartedEvent;
        const meetingId = event.call.custom?.meetingId;

        // 2 
        console.log(`[Webhook] Session Started. Meeting ID: ${meetingId}`); // <-- LOG 2: Check Meeting ID

        if (!meetingId) {
            return NextResponse.json({ error: "Missing meetingId" }, { status: 400 });
        }

        const [existingMeeting] = await db
            .select()
            .from(meetings)
            .where(
                and(
                    eq(meetings.id, meetingId),
                    not(eq(meetings.status, "completed")),
                    not(eq(meetings.status, "active")),
                    not(eq(meetings.status, "cancelled")),
                    not(eq(meetings.status, "processing"))
                )
            );

        if (!existingMeeting) {

            // 3 
            console.error(`[Webhook ERROR] Meeting not found for ID: ${meetingId}`);
            return NextResponse.json({ error: "Meeting not found" }, { status: 404 });
        }

        // 4

        console.log(`[Webhook] Found Meeting in DB. Agent ID: ${existingMeeting.agentId}`); // <-- LOG 3: Confirm DB lookup

        await db
            .update(meetings)
            .set({
                status: "active",
                startedAt: new Date(),
            })
            .where(eq(meetings.id, existingMeeting.id));

        const [existingAgent] = await db
            .select()
            .from(agents)
            .where(eq(agents.id, existingMeeting.agentId));


        if (!existingAgent) {
            // 5
            console.error(`[Webhook ERROR] Agent not found for ID: ${existingMeeting.agentId}`);
            return NextResponse.json({ error: "Agent not found" }, { status: 404 });
        }

        // <-- CRITICAL LOGS 4 & 5: Check instructions and key existence
        const instructions = existingAgent.instructions;
        console.log(instructions);
        console.log(`[Webhook] Found Agent: ${existingAgent.id}. Instructions Length: ${instructions?.length ?? 0}`);

        const call = streamVideo.video.call("default", meetingId);

        console.log(`[Webhook] Calling connectOpenAi...`); // <-- LOG 6: Before SDK call
        const realtimeClient = await streamVideo.video.connectOpenAi({
            call,
            openAiApiKey: process.env.OPENAI_API_KEY!,
            agentUserId: existingAgent.id,
            model:"gpt-4o-realtime-preview-2025-06-03",
        });
        console.log(`[Webhook] connectOpenAi SUCCESS`);

        console.log(`[Webhook] connectOpenAi SUCCESS. Updating session instructions...`); // <-- LOG 7: After SDK call
        await realtimeClient.updateSession({
            instructions: existingAgent.instructions,
        });

        realtimeClient.on("conversation.item.input_audio_transcription_completed", (event:any) => {
            console.log(`[Webhook] User said: ${event.transcript}`);
        });
        realtimeClient.on("conversation.item.created", (event:any) => {
            console.log(`[Webhook] Agent response:`, event);
        });

        console.log(`[Webhook] Agent setup complete!`);
    } else if (eventType === "call.session_participant_left") {
        const event = payload as CallSessionParticipantLeftEvent;
        const meetingId = event.call_cid.split(":")[1];

        console.log(`[Webhook] Handled participant left event.`);

        if (!meetingId) {
            return NextResponse.json({ error: "Missing meetingId" }, { status: 400 });
        }

        const call = streamVideo.video.call("default", meetingId);
        await call.end();
    } else if (eventType === "call.session_ended") {
        const event = payload as CallEndedEvent;
        const meetingId = event.call.custom?.meetingId;

        if (!meetingId) {
            return NextResponse.json({ error: "Missing meetingId" }, { status: 400 });
        }

        await db
            .update(meetings)
            .set({
                status: "processing",
                endedAt: new Date(),
            })
            .where(and(eq(meetings.id, meetingId), eq(meetings.status, "active")));

    } else if (eventType === "call.transcription_ready") {
        const event = payload as CallTranscriptionReadyEvent;
        const meetingId = event.call_cid.split(":")[1];

        const [updatedMeeting] = await db
            .update(meetings)
            .set({
                transcriptUrl: event.call_transcription.url,
            })
            .where(eq(meetings.id, meetingId))
            .returning();

        // TODO: call Inngest background job to summarize the transcript
        

        if (!updatedMeeting) {
            return NextResponse.json({ error: "Meeting not found" }, { status: 400 });
        }

       
    } else if (eventType === "call.recording_ready") {

        const event = payload as CallRecordingReadyEvent;
        const meetingId = event.call_cid.split(":")[1];

        await db
            .update(meetings)
            .set({
                recordingUrl: event.call_recording.url,
            })
            .where(eq(meetings.id, meetingId));


    }

    return NextResponse.json({ status: "ok" });
}
    

Versions I am using

    "openai": "^6.6.0"
    "@stream-io/node-sdk": "^0.4.24",
    "@stream-io/openai-realtime-api": "^0.3.3",
    "@stream-io/video-react-sdk": "^1.18.0",
openai-api getstream-io