Auto-connects on construction. When you create a Pinecall instance with an API key, it connects immediately — no need to call connect().
Constructor#
new Pinecall(options)| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | PINECALL_API_KEY env var | Your Pinecall API key. Auto-read from env if not provided. |
apiUrl | string | wss://voice.pinecall.io | Server URL |
autoReconnect | boolean | true | Auto-reconnect on disconnect |
promptsDir | string | "prompts" | Prompts directory for setPromptFile |
Example#
// Reads PINECALL_API_KEY from env automatically
const pc = new Pinecall();
// Or pass explicitly
const pc = new Pinecall({ apiKey: "pk_..." });Agents can be created immediately — they queue and register when the connection is ready:
const pc = new Pinecall();
const agent = pc.agent("support", { /* ... */ }); // works before connectedMethods#
ready#
Promise<void> that resolves when the connection is established. Use it when you need to wait for the connection before proceeding (e.g. before dialing an outbound call).
await pc.ready;
const call = await agent.dial({ to: "+14155551234" });connect()#
Manually open the WebSocket connection. Rarely needed — the constructor auto-connects when an API key is present. Idempotent (safe to call multiple times).
await pc.connect();disconnect()#
Gracefully close the connection.
await pc.disconnect();agent(id, config?)#
Create or retrieve an agent. If an agent with this ID already exists, returns it (idempotent).
const agent = pc.agent("support", {
voice: "elevenlabs/sarah",
language: "en",
llm: "openai/gpt-5.4-nano",
stt: "deepgram/flux",
prompt: "You are a support agent. Be concise.",
greeting: "Hi! How can I help you today?",
phoneNumber: "+13186330963",
});AgentConfig fields:
| Field | Type | Description |
|---|---|---|
voice | string | VoiceConfig | TTS voice shortcut (e.g. elevenlabs/sarah) |
language | string | BCP-47 language code |
stt | string | STTConfig | STT shortcut (e.g. deepgram/flux) |
llm | string | LLMConfig | LLM shortcut (e.g. openai/gpt-5.4-nano) or full config |
prompt | string | System prompt for the LLM |
promptVars | Record<string, string> | Default {{var}} values, seeded at registration so nothing ever renders as a literal {{VAR}} |
preparing | boolean | { timeoutMs } | Opt in to the pre-turn barrier: the server holds each generation while your call.preparing handler refreshes per-turn variables |
greeting | string | { text, addToHistory? } | (call) => string | Greeting spoken on inbound calls. Added to LLM history by default. |
tools | Tool[] | Declarative tool definitions created with tool() |
phoneNumber | string | PhoneNumberConfig | Phone number or SIP URI to register (Twilio) |
phoneNumbers | Array<string | PhoneNumberConfig> | Multiple phone numbers with per-number config (e.g. one per language) |
whatsapp | WhatsAppChannelConfig[] | WhatsApp channels (Meta Cloud API credentials) |
sessionLimits | object | Session timeout config (see Session Limits) |
allowedOrigins | string[] | Allowed origins for public browser token access (see Security) |
Dynamic greetings with a function:
greeting: async (call) => {
const customer = await db.findByPhone(call.from);
return `Hi ${customer.name}! How can I help?`;
},Greeting without LLM history (e.g. a standalone announcement):
greeting: { text: "Welcome! Please hold.", addToHistory: false },See Agent for full API reference.
getAgent(id)#
Look up an agent by ID. Returns Agent | undefined.
const mara = pc.getAgent("mara");removeAgent(id)#
Unregister an agent. Returns boolean indicating whether the agent existed.
const removed = pc.removeAgent("mara");createToken(channel, agentId, metadata?)#
Generate a short-lived, single-use token for browser WebRTC or chat connections. Used to mint tokens for browsers.
const token = await pc.createToken("webrtc", "mara");
// { token, server, expiresIn }For an agent this client owns, the mint is ordered after the server's registration ack (see agent.ready) — so registering and minting in consecutive statements works, with no delay of your own. Agents owned by another process are minted straight through.
Sealed session metadata — pass a third argument to bake trusted context into the token:
const token = await pc.createToken("chat", "mara", { userId: "u_123", plan: "pro" });The metadata is sealed into the signed token on your server, so the browser cannot forge or alter it. It surfaces as call.metadata in your call.started handler — use it for per-user / multi-tenant context you can trust (auth identity, plan, tenant id). Works identically for "webrtc" and "chat".
With an
Agentinstance, useagent.createToken(channel, metadata?)(theagentIdis implicit).
⚠️ This is not the client-supplied
metadataprop on the widget /VoiceSession— that is set in the browser and can be forged. For anything used in authorization, seal it in the token here.
Multi-tenant pattern: sealed metadata lets ONE shared agent serve every tenant — the logged-in user's identity (tenant id, role, …) rides per-call in
call.metadata, so tools scope by it in code. See Multi-Tenant → sealed token metadata.
See Security for the full token model.
createToken("stream", agents)#
The same method also mints observation tokens for the call log: pass "stream" and one agent slug — or a list of slugs — and the returned token lets its holder observe those agents' calls (live tail, replay, history) without participating. The agent set is sealed into the token's signature; the browser cannot widen it.
const t = await pc.createToken("stream", "mara"); // one agent
const t = await pc.createToken("stream", ["mara", "sales"]); // an agent set
// read-only: no supervise verbs, narrowed to one call
const t = await pc.createToken("stream", "mara", undefined, { scope: "observe", callId });
// → { token, server, expiresIn }The fourth argument is { scope?: "observe" | "participate" | "supervise"; callId?: string }.
Consume it in the browser with @pinecall/web/log/react (useAgentCalls, useCall), or in Node with pc.observe(). See Observe calls.
observe(options)#
Read the call log over SSE — the one way to observe a
call from Node. Opens GET /v1/calls/{id}/events (or
/v1/agents/{slug}/calls) with Accept: text/event-stream, feeds every
envelope into the same CallLogView reducer the browser uses, and exposes it
three ways: the reduced state, on() listeners, and for await.
No WebSocket is opened — observation is read-only by construction.
const obs = pc.observe({ agent: "mara", types: ["custom", "call.ended"] });
obs.on("custom", (name, value, entry) => console.log(entry.call, name, value));
obs.on("entry", (entry, state) => {});
obs.on("finish", ({ reason, lastSeq }) => {});
for await (const entry of obs) console.log(entry.seq, entry.type);
obs.state // the reduced CallLogState
obs.lastSeq // the resume cursor
obs.dropped // entries the ITERATOR never saw (state is never affected)
await obs.done;
obs.close();| option | default | what it is |
|---|---|---|
call / agent | — | exactly one. call = one call's log; agent = the agent's lifecycle log (which never ends) |
after | 0 | start cursor |
token | minted | omit and one is minted with this client's API key ({ channel: "stream", scope: "observe" }) — which needs an agent, so observe({ call }) without a token must also pass agent |
types / durable | — | the server-side filters, plus the always-pass set |
queueLimit | 1024 | bound on the async iterator's buffer; on overflow the oldest entries are dropped and counted in obs.dropped. state and on("entry") are never lossy |
idleReconnect | "auto" | half-open detection |
reconnect | true | false disables auto-reconnect |
signal | — | aborting it is exactly close() |
server / apiUrl / apiKey | the client's | overrides |
onError | — | transport-level failures; state is never faked into the view |
See Observe calls for the resume rules, the filters and the browser side.
stream(res?, options?)#
Open an SSE stream of agent events — in-process only (the HTTP endpoint must live in this same process, and there is no replay, no cursor, no history, and only the calls this process handles; for anything a user sees, read the call log instead). Works with any framework — returns a Web API Response or writes to a Node.js ServerResponse.
// Web API (Remix, Next.js, Hono, Bun)
app.get("/events", () => pc.stream());
// Express / Node.js
app.get("/events", (req, res) => pc.stream(res));
// Filtered to specific agents
app.get("/events", () => pc.stream({ agents: ["mara", "support"] }));
app.get("/events", (req, res) => pc.stream(res, { agents: ["mara"] }));See Multi-tenant guide for the filtering pattern.
Events#
Subscribe via pc.on(event, handler).
| Event | Signature | When |
|---|---|---|
connected | () | WebSocket auth succeeded |
disconnected | (reason) | Connection closed |
reconnecting | (attempt, delay) | Auto-reconnect attempt N |
error | (err) | Protocol or transport error |
pc.on("connected", () => console.log("Live"));
pc.on("disconnected", (reason) => console.log("Down:", reason));
pc.on("reconnecting", (n) => console.log(`Retry ${n}`));
pc.on("error", (err) => console.error(err));
