Pinecall

Agent

Owns channels, routes call events, stores defaults, dials outbound calls.

Creation#

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

const lookupOrder = tool({
  name: "lookupOrder",
  description: "Look up an order by ID",
  schema: z.object({ id: z.string() }),
  execute: async ({ id }) => ({ status: "shipped", eta: "today" }),
});

const agent = pc.agent("my-agent", {
  voice: "elevenlabs/sarah",
  language: "es",
  stt: "deepgram/flux",
  llm: "openai/gpt-5-chat-latest",
  prompt: "System prompt with {{template_vars}}.",
  greeting: "Hello! How can I help you today?",
  phoneNumber: "+13186330963",
  tools: [lookupOrder],
});
Config fieldTypeDescription
voicestring | VoiceConfigTTS provider — shortcut or full config
languagestringBCP-47 language code. Non-English auto-selects ElevenLabs eleven_multilingual_v2
flashbooleanKeep ElevenLabs eleven_flash_v2_5 on a non-English agent (lowest latency/cost) instead of the multilingual auto-default. ElevenLabs-only; see TTS Providers
sttstring | STTConfigSTT provider — shortcut or full config
llmLLMConfigLLM provider, model, prompt, enabled flag
toolsTool[]Declarative tools created with tool() + Zod schemas (auto-executed)
phoneNumberstring | PhoneNumberConfigPhone number to register (E.164 or SIP URI)
phoneNumbersArray<string | PhoneNumberConfig>Multiple numbers with per-number config
whatsappWhatsAppChannelConfig[]WhatsApp channels to register
historyHistoryStoreConversation persistence (see History)
sessionLimitsSessionLimitsDuration / idle timeout config
interruptionInterruptionConfigEnergy thresholds for barge-in
analysisAnalysisConfigAudio metrics streaming
allowedOriginsstring[]Public token access (see Security)

See Reference → Providers for full provider configs.

Phone numbers#

addPhoneNumber(number, config?)#

Register a phone number or SIP URI. Idempotent — calling again with the same number updates its config.

agent.addPhoneNumber("+13186330963");
agent.addPhoneNumber("sip:bot@trunk.twilio.com");

// Per-number config overrides
agent.addPhoneNumber("+34911234567", {
  voice: "elevenlabs/valentina",
  language: "es",
});

removePhone(number)#

Unregister a phone number.

agent.removePhone("+34911234567");

WhatsApp#

addWhatsapp(config)#

Register a WhatsApp channel. Idempotent.

agent.addWhatsapp({
  phoneNumberId: "123456789012345",
  accessToken: "EAABx...",
  verifyToken: "my-secret",
  appSecret: "abc123...",
});

See WhatsApp guide for full config.

removeWhatsapp(phoneNumberId)#

Unregister a WhatsApp channel.

agent.removeWhatsapp("123456789012345");

Config & hot-reload#

update(opts)#

Hot-reload the agent's defaults. Affects all future calls — existing calls keep their current config.

agent.update({ voice: "elevenlabs/claire", language: "fr" });
agent.update({ stt: "gladia" });
agent.update({ llm: "openai/gpt-5-chat-latest", prompt: "..." });

configureSession(callId, opts)#

Update config for a live call (equivalent to call.update()).

agent.configureSession("CA7ec...", { language: "es" });

getConfig()#

Returns the current AgentConfig.

const cfg = agent.getConfig();

Outbound calls#

dial(options)#

Make an outbound call. Returns Promise<Call>.

const call = await agent.dial({
  to: "+14155551234",
  from: "+13186330963",
  greeting: "Hi! This is a follow-up call.",
  metadata: { appointmentId: "appt_001" },
  config: { voice: "cartesia/yumiko", language: "ar" },
});
FieldTypeRequiredDescription
tostringDestination number (E.164)
fromstringCaller ID — auto-resolved if agent has one phone channel. Required when multiple.
greetingstringText the server speaks when callee picks up
metadataobjectCustom data attached to the call
configobjectPer-call config override (voice, STT, language)

See Outbound Calls guide for the full pattern.

Tokens#

createToken(channel, metadata?)#

Mint a short-lived, single-use token for browser WebRTC or chat. Scoped to this agent (the agent-form shortcut for pc.createToken(channel, agentId, metadata?)).

const token = await agent.createToken("webrtc");
// { token, server, expiresIn }

Sealed session metadata — pass a second argument to bake trusted context into the token:

const token = await agent.createToken("chat", { userId: "u_123", role: "admin" });

The metadata is sealed into the signed token on your server, so the browser can't forge or alter it — it surfaces as call.metadata in your call.started handler. Use it for per-user / multi-tenant identity you can trust.

⚠️ Arg position differs by form: agent.createToken(channel, metadata) (metadata 2nd, agentId implicit) vs pc.createToken(channel, agentId, metadata) (metadata 3rd). It is not the forgeable client-side metadata prop on the widget / VoiceSession. See Multi-Tenant → sealed token metadata and Security.

Dev mode#

routeCallers(numbers)#

Route phone and WhatsApp messages from these numbers to this agent (instead of any other agent registered on the same channel). Used for dev mode isolation.

agent.routeCallers(["+34600123456", "+34612345678"]);

See Dev mode guide.

Human-in-the-loop#

Pause the AI so a human can take over the conversation. Works on WhatsApp and (soon) voice/chat channels.

pause(target?)#

Pause the agent. While paused, incoming messages are forwarded to the SDK but the LLM doesn't respond.

// Pause a specific session
agent.pause("wa-abc123");

// Pause all sessions with a contact
agent.pause({ contact: "+34612345678" });

// Pause the entire agent
agent.pause();

resume(target?)#

Resume the AI after a pause. Global resume clears all session and contact pauses.

agent.resume("wa-abc123");
agent.resume({ contact: "+34612345678" });
agent.resume();

sendMessage(opts)#

Send a message as the human operator. The message is delivered through the channel (e.g. WhatsApp) and added to LLM history so the AI has context when resumed.

agent.sendMessage({
  sessionId: "wa-abc123",
  text: "Hi, I'm taking over this conversation.",
});
FieldTypeRequiredDescription
sessionIdstringTarget session ID (e.g. wa-abc123)
textstringMessage body

See Human Takeover guide for the full pattern.

Calls#

call(callId)#

Look up a live Call by ID. Returns Call | undefined.

const call = agent.call("CA7ec...");

Observability#

stream(res?)#

Open an SSE stream of this agent's events. Same shape as pc.stream() but scoped to one agent.

app.get("/events", () => agent.stream());
app.get("/events", (req, res) => agent.stream(res));

Events#

Subscribe via agent.on(event, handler). All call-scoped events include call as the last argument.

Lifecycle#

EventSignatureWhen
call.started(call)New call connected
call.ended(call, reason)Call disconnected

User speech#

EventSignatureWhen
speech.started(event, call)User began speaking (VAD)
speech.ended(event, call)User stopped speaking (VAD)
user.speaking(event, call)Interim STT transcript (updates live)
user.message(event, call)Final confirmed user text

Turns#

EventSignatureWhen
eager.turn(turn, call)Early turn signal (low-latency response)
turn.end(turn, call)Final turn signal
turn.continued(event, call)User kept talking (auto-aborts active streams)

Bot speech#

EventSignatureWhen
bot.speaking(event, call)Bot started speaking a message
bot.word(event, call)Individual word as TTS plays it
bot.finished(event, call)Bot finished speaking a message
bot.interrupted(event, call)Bot was cut off by user

Protocol#

EventSignatureWhen
message.confirmed(event, call)Server acknowledged bot message
llm.toolCall(data, call)Server-side LLM requests a tool call
session.idleWarning(event, call)Warning — user hasn't spoken, call will timeout soon
session.timeout(event, call)Session timeout fired (max duration / idle)

WhatsApp#

EventSignatureWhen
whatsapp.sessionStarted(event)New WhatsApp conversation started
whatsapp.message(event)Incoming WhatsApp message received
whatsapp.response(event)Agent sent a WhatsApp response
whatsapp.status(event)Message delivery status

See Events reference for full event data shapes.

Human-in-the-loop#

EventSignatureWhen
session.paused(event)AI paused for a session, contact, or globally
session.resumed(event)AI resumed

See Human Takeover guide.

Escape hatch#

send(data)#

Send a raw protocol message. Use only when no higher-level method covers your case.

agent.send({ type: "custom.command", payload: { /* ... */ } });

What's next#