Pinecall

Call

Per-session handle. Speak, control, update, read state.

agent.on("call.started", (call) => {
  // call is a Call instance
});

Properties#

call.id              // "CA7ec979f5..." — unique call ID
call.from            // "+13186330963" or "sip:..."
call.to              // destination number / URI
call.direction       // "inbound" | "outbound"
call.transport       // "phone" | "webrtc" | "chat" | "whatsapp" | "unknown"
call.metadata        // sealed token metadata (createToken), dial() metadata, or channel context
call.transcript      // [{ role: "user", content: "..." }, ...] — user + assistant only
call.messages        // full LLM history (populated on call.ended)
call.currentBotText  // live preview of what the bot is saying (accumulated bot.word events)
call.duration        // seconds (populated on call.ended)
call.startedAt       // epoch seconds
call.endedAt         // epoch seconds
call.reason          // "hangup" | "timeout" | ...

metadata#

Arbitrary context attached to the session, available in call.started and throughout the call. It comes from one of:

  • Sealed token metadata (browser WebRTC / chat) — passed to createToken(channel, agentId, metadata) on your server and sealed into the signed token. Trusted: the browser can't forge it, so it's safe for identity / plan / tenant.
  • dial() metadata (outbound calls) — passed when you place the call.
  • Channel context — provider-supplied fields for the transport.
agent.on("call.started", (call) => {
  if (call.metadata?.plan === "pro") enablePremiumTools(call);
});

The client-supplied metadata prop on the browser widget / VoiceSession also lands here, but is set in the browser — don't trust it for authorization; seal that in the token instead.

currentBotText#

A live preview of what the bot is currently saying. Built automatically from bot.word events — grows word-by-word as TTS plays, resets on each new bot.speaking, clears after bot.finished or bot.interrupted.

agent.on("bot.word", (event, call) => {
  updateSubtitle(call.currentBotText); // "¡Hola!" → "¡Hola! Estoy" → "¡Hola! Estoy bien,"
});

Speech#

say(text, opts?)#

Speak text immediately. Standalone — no in_reply_to tracking. Use for greetings and proactive announcements.

call.say("Hello! How can I help?");

Pass { addToHistory: true } to inject the text into the server-side LLM conversation history as an assistant message. This way the model knows what was said and won't repeat it.

agent.on("call.started", async (call) => {
  const customer = await db.findByPhone(call.from);
  call.say(`Hi ${customer.name}! How can I help?`, { addToHistory: true });
});

Tip: The greeting field in pc.agent() is syntactic sugar for call.say(text, { addToHistory }) inside a call.started handler. Use greeting for convenience, or call.say() directly for full control.

reply(text)#

Reply to the latest user message. Auto-tracks in_reply_to. Use when responding to what the user just said.

call.reply("Sure, let me look that up for you.");

replyStream(turn?)#

Open a token-by-token stream for LLM responses. TTS starts as soon as a sentence boundary is detected.

const stream = call.replyStream(turn);

for await (const token of llm.stream(prompt)) {
  if (stream.aborted) break; // user interrupted
  stream.write(token);
}
stream.end();

See ReplyStream for details.

toolResult(msgId, results)#

Send tool results back to the server-side LLM. When using tool(), the SDK calls this automatically — you don't need to call it yourself.

// Auto-called by tool() — you rarely need this directly.
// Only use for advanced cases where you bypass tool() auto-execution.
call.toolResult(msgId, [
  { toolCallId: "tc_abc", result: { status: "shipped" } },
]);

cancel(msgId?)#

Cancel a specific bot message (by ID) or the current one (if no ID).

call.cancel();             // cancel current
call.cancel("msg_abc123"); // cancel specific

clear()#

Flush all queued TTS audio. Stops the bot mid-speech.

call.clear();

Call control#

hangup()#

End the call.

call.hangup();

forward(to, opts?)#

Transfer the call to another number.

call.forward("+15558675309");

sendDTMF(digits)#

Send DTMF tones. Use 0-9, *, #.

call.sendDTMF("1234#");

hold() / unhold()#

Put the call on hold (plays hold music, mutes mic) and resume.

call.hold();
// ...later
call.unhold();

mute() / unmute()#

Mute and unmute the mic. Transcripts are buffered while muted; on unmute(), call.unmuted fires with the buffered transcript.

call.mute();
call.unmute();

streamSSE(res, opts?)#

Stream call events as Server-Sent Events to an HTTP response. Used for "Call Me" flows where the browser needs a live transcript of an outbound call.

app.post("/api/call-me", async (req, res) => {
  const call = await agent.dial({
    to: req.body.phone,
    greeting: "Hi! You asked me to call you.",
  });
  call.streamSSE(res);
});

Sets SSE headers automatically. Streams: call.started, bot.word, bot.confirmed, user.speaking, user.message, tool.call, call.ended. Sends :ping keepalives every 25s. Cleans up listeners on client disconnect.

OptionTypeDescription
greetingstringOverride the greeting text sent as first bot.confirmed. Defaults to call.greeting (set by dial()).

See Voice Widget → Call Me for the full pattern.

Mid-call configuration#

update(opts)#

Change voice, STT, or language. Takes effect on the next LLM turn or TTS output.

call.update({ voice: "elevenlabs/valentina", language: "es" });

setPrompt(text)#

Replace the system prompt for this call only. The agent's default prompt is unchanged.

call.setPrompt("You are now in escalation mode. Be more formal.");

setPromptVars(vars)#

Set {{variable}} values in the prompt template.

await call.setPromptVars({
  customer_name: "Maria",
  tier: "premium",
});

addContext(text)#

Append context after the system prompt. Useful for injecting CRM data, tool results, or live state.

await call.addContext(`Recent orders:\n- ORD-001: shipped\n- ORD-002: pending`);

You can call addContext multiple times during a call — each call appends.

setPromptFile(path)#

Load a prompt from a file and set it. Equivalent to readFile + setPrompt.

await call.setPromptFile("./prompts/escalation.md");

Conversation history#

getHistory()#

Fetch the current conversation messages in OpenAI format.

const messages = await call.getHistory();
// [{ role: "system", content: "..." }, { role: "user", content: "..." }, ...]

addHistory(msgs)#

Inject messages into the history. Useful for CRM context or seeding past conversation.

await call.addHistory([
  { role: "user", content: "I called yesterday about my order" },
  { role: "assistant", content: "Yes, I see it shipped this morning." },
]);

setHistory(msgs)#

Replace the entire conversation history.

await call.setHistory([
  { role: "system", content: "You are now in escalation mode." },
]);

clearHistory()#

Clear all messages. The system prompt is preserved.

call.clearHistory();

Common patterns#

Greet on call.started#

agent.on("call.started", (call) => {
  if (call.direction === "inbound") {
    call.say("Hello! How can I help?");
  }
});

Persist transcripts on call.ended#

agent.on("call.ended", async (call, reason) => {
  await db.calls.create({
    id: call.id,
    from: call.from,
    to: call.to,
    direction: call.direction,
    transport: call.transport,
    duration: call.duration,
    reason,
    transcript: call.transcript,
    messages: call.messages,
  });
});

Transfer when escalation requested#

import { tool } from "@pinecall/sdk";
import { z } from "zod";

const transferToHuman = tool({
  name: "transferToHuman",
  description: "Escalate to a human agent.",
  schema: z.object({}),
  execute: async (_, call) => {
    call.say("Connecting you now.");
    call.forward("+15558675309");
    return { transferred: true };
  },
});

What's next#