PurpleCallio documentation
AI Voice Agents
AI voice agents answer your users by voice in real time. You configure the agent in the dashboard, your backend creates a short-lived session, and the browser streams audio through PurpleCallio to the voice model. They unlock after your first verified credit purchase.
Create an agent
In Dashboard → AI Agents, create an agent in a project and set its instructions, optional welcome message, voice, language, model provider and maximum call duration (30 seconds to 30 minutes). Each change creates a new version; sessions use the version current when they were created. Try it in the Playground tab before going live.
Add tools
Tools let the agent call your HTTPS endpoints mid-conversation, for example to look up an order. Each tool has a name, a description the model reads, a parameters schema, an endpoint, a method (POST or GET), a timeout and an optional confirmation step. Every request is signed with the tool's bj_tool_ secret.
How a call flows
Your backend calls POST /ai/sessions with its API key and gets a single-use bj_ai_ token. The browser connects with that token, the agent greets the user, and audio, transcripts and tool activity stream live. The call ends when either side hangs up, the maximum duration is reached, or your backend ends the session.
Tools: the JSON-schema subset
Each tool declares its arguments with a small JSON-schema subset that every voice model provider can represent:
- The root is { "type": "object", "properties": {…}, "required": [...] } with an optional description.
- Each property has a type of string, number, integer or boolean, plus an optional description and enum (1–64 values of that type).
- No nesting, arrays, $ref or other keywords. At most 32 properties; names match ^[a-zA-Z][a-zA-Z0-9_]{0,63}$; the schema is at most 16 KB.
- Arguments the model sends are checked against the schema before your endpoint is called. String arguments are capped at 4,096 characters.
{
"type": "object",
"description": "Look up an order by its id",
"properties": {
"orderId": { "type": "string", "description": "The order id, e.g. ord_123" },
"channel": { "type": "string", "enum": ["web", "store"] },
"notify": { "type": "boolean" }
},
"required": ["orderId"]
}Create a session on your server
client.ai.createSession() returns { sessionId, callId, token, expiresAt, connectUrl }. Send only the token to the browser.
// Your backend: the API key never leaves the server.
import express from "express";
import { PurpleCallioClient } from "@purplecallio/sdk";
const client = new PurpleCallioClient({
apiKey: process.env.PURPLECALLIO_API_KEY!,
apiUrl: "https://purplecallio.com/api",
});
const app = express();
app.post("/api/ai-session", requireLogin, async (req, res) => {
const session = await client.ai.createSession({
agentId: process.env.PURPLECALLIO_AGENT_ID!,
participant: { id: req.user.id, name: req.user.name },
metadata: { plan: req.user.plan }, // optional, max 2 KB JSON
});
// Hand the browser only the short-lived bj_ai_ token.
res.json({ token: session.token });
});
// Later, from your backend:
// await client.ai.getSession(sessionId);
// await client.ai.endSession(sessionId);Browser SDK
PurpleCallioAgentCallopens the microphone, streams audio to PurpleCallio and plays the agent's reply. Tokens are single-use: create a new session for every call.
import { PurpleCallioAgentCall } from "@purplecallio/sdk";
async function talkToAgent() {
const { token } = await fetch("/api/ai-session", { method: "POST" }).then((r) => r.json());
const call = new PurpleCallioAgentCall({
token,
apiUrl: "https://purplecallio.com", // your PurpleCallio server origin (required)
});
call.on("transcript", ({ role, text, final }) => render(role, text, final));
call.on("agentState", (state) => setIndicator(state)); // listening | thinking | speaking
call.on("tool.confirmation_required", ({ id, name }) =>
call.confirmTool(id, window.confirm(`Allow the agent to run ${name}?`)),
);
call.on("error", (err) => console.warn(err.code));
call.on("ended", ({ reason }) => console.log("ended:", reason));
await call.start(); // call from a click handler so audio can play
// call.mute(); call.unmute(); call.end();
}React hook and component
@purplecallio/react 0.2 adds usePurpleCallioAI and a ready-made <PurpleCallioAI />. Both call your getToken on every start, so your API key stays on your server.
import { usePurpleCallioAI } from "@purplecallio/react";
const getToken = () =>
fetch("/api/ai-session", { method: "POST" })
.then((r) => r.json())
.then((s) => s.token);
export function SupportAgent() {
const ai = usePurpleCallioAI({
getToken,
apiUrl: "https://purplecallio.com", // required
});
if (ai.status === "idle" || ai.status === "ended") {
return <button onClick={ai.start}>Talk to support</button>;
}
return (
<div>
<p>{ai.status} · {ai.agentState} · {ai.durationSeconds}s</p>
<button onClick={ai.isMuted ? ai.unmute : ai.mute}>{ai.isMuted ? "Unmute" : "Mute"}</button>
<button onClick={ai.end}>End</button>
{ai.pendingConfirmations.map((t) => (
<button key={t.id} onClick={() => ai.confirmTool(t.id, true)}>Allow {t.name}</button>
))}
{ai.error && <p role="alert">{ai.error.code}</p>}
{ai.transcript.map((l) => <p key={l.key}>{l.role}: {l.text}</p>)}
</div>
);
}import { PurpleCallioAI } from "@purplecallio/react";
export function SupportWidget() {
return (
<PurpleCallioAI
apiUrl="https://purplecallio.com" // required
getToken={() => fetch("/api/ai-session", { method: "POST" }).then((r) => r.json())}
className="my-agent"
startLabel="Talk to support"
/>
);
}Style the component with className and the bj-ai, bj-ai-status, bj-ai-indicator, bj-ai-controls, bj-ai-error, bj-ai-confirm and bj-ai-transcript classes.
Verify tool requests
PurpleCallio calls your tool endpoint with a signed request. POST tools receive this JSON body; GET tools receive the same JSON, URL-encoded, in the payload query parameter, and that string is what is signed. Dashboard test calls have sessionId and callId set to null and "test": true.
{
"tool": "orderStatus",
"arguments": { "orderId": "ord_123" },
"sessionId": "…",
"callId": "…",
"agentId": "…",
"participant": { "id": "user_42", "name": "Sam" }
}| Header | Value |
|---|---|
| X-PurpleCallio-Event | ai.tool_call |
| X-PurpleCallio-Timestamp | Unix time in seconds when the request was signed |
| X-PurpleCallio-Signature | sha256=<hex HMAC-SHA256 of "<timestamp>.<body>">, keyed with the tool signing secret (bj_tool_…) |
Reply with Content-Type: application/json; the agent sees your JSON as the tool result.
const crypto = require("crypto");
const express = require("express");
const SECRET = process.env.PURPLECALLIO_TOOL_SECRET; // bj_tool_…
const MAX_SKEW_SECONDS = 300;
function verifyPurpleCallio(rawBody, headers) {
const timestamp = headers["x-purplecallio-timestamp"];
const signature = headers["x-purplecallio-signature"] || "";
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_SECONDS) {
return false; // missing or stale: possible replay
}
const expected =
"sha256=" + crypto.createHmac("sha256", SECRET).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// Verify against the raw bytes, before any JSON parsing.
app.post("/tools/order-status", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyPurpleCallio(raw, req.headers)) return res.status(401).json({ error: "bad signature" });
const { arguments: args } = JSON.parse(raw);
res.json({ status: lookUpOrder(args.orderId) });
});
// GET tools: the signed string is the `payload` query parameter.
app.get("/tools/order-status", (req, res) => {
const raw = String(req.query.payload || "");
if (!verifyPurpleCallio(raw, req.headers)) return res.status(401).json({ error: "bad signature" });
const { arguments: args } = JSON.parse(raw);
res.json({ status: lookUpOrder(args.orderId) });
});Tool endpoint network rules
- Endpoints must be https:// URLs. Plain http is refused.
- The host is resolved before every request, and the call is refused if any address is private, loopback, link-local (including 169.254.169.254 cloud metadata), CGNAT, multicast, documentation or another reserved range, for IPv4 and IPv6.
- The connection is pinned to the checked address, so DNS rebinding cannot redirect it.
- Redirects are not followed. Responses must be JSON and at most 64 KB, and must arrive within the tool timeout (500 ms–30 s, capped at 8 s by default).
Availability and pricing
AI voice agents are available after an eligible credit purchase. AI calls are billed in credits per minute of call time (not per participant), rounded up once per call. A session cannot start without enough credits, and an active call is never cut off if the wallet runs low. Sessions that fail to connect are not billed. See credit pricing.
Prepaid credit pricing
PurpleCallio uses prepaid credits with no recurring subscription plan. Usage is measured in participant-minutes and each media type — audio, video and screen sharing — consumes credits at its own rate. Screen sharing is its own category, not a surcharge on video.
Loading current credit rates…
Error codes
Errors are returned as { code, message } and surface in the SDK as an Error with a stable code.
| Code | Meaning |
|---|---|
| AI_DISABLED | AI voice agents are not enabled on this deployment. |
| AI_PAID_PLAN_REQUIRED | AI voice agents unlock after your first verified credit purchase. |
| AI_AGENT_NOT_FOUND | The agent id does not exist or belongs to another account. |
| AI_AGENT_DISABLED | The agent is disabled in the dashboard. |
| AI_QUOTA_EXCEEDED | The daily or monthly AI minute limit is used up. |
| AI_CONCURRENCY_LIMIT | Too many AI calls are running for the account or platform. Retry shortly. |
| AI_INSUFFICIENT_CREDITS | The credit balance cannot cover a call. Top up or renew. |
| AI_SESSION_CREATE_FAILED | The session could not be created. Retry with backoff. |
| AI_PROVIDER_UNAVAILABLE | The voice model provider is unreachable. The session is not billed. |
| AI_CONNECTION_FAILED | The live connection to the agent failed. |
| AI_SESSION_EXPIRED | The token was not used within its time to connect (5 minutes by default). |
| AI_SESSION_ENDED | The session already ended. Create a new one; tokens are single-use. |
| AI_MAX_DURATION_REACHED | The call hit the agent’s maximum duration and was ended. |
| AI_TOOL_FAILED | A tool endpoint failed, was refused or returned an invalid response. |
| AI_TOOL_TIMEOUT | A tool endpoint did not answer within its timeout. |
| AI_MICROPHONE_DENIED | Browser SDK only: the user denied microphone access. |
| AI_MICROPHONE_UNAVAILABLE | Browser SDK only: no microphone was found. |
| AI_AUDIO_UNSUPPORTED | Browser SDK only: the browser lacks the audio APIs the call needs. |
Limits
Default limits; contact support if you need higher ones.
| Limit | Default |
|---|---|
| Concurrent AI calls per account | 3 |
| Platform-wide concurrent AI calls | 50 |
| AI minutes per account per day | 120 |
| AI minutes per account per month | 1,000 |
| Maximum call duration | 30 minutes (agent default 10 minutes, minimum 30 seconds) |
| Time to connect after creating a session | 5 minutes |
| Tool calls per session | 20 |
| Tool timeout | 8 seconds |
| Tool response size | 64 KB |
| Stored transcript messages per call | 500 |
Security notes
- Create sessions only from your backend with your bj_live_ API key, and only after you have authenticated your own user. The browser receives a bj_ai_ token that works for one session and expires if unused.
- Provider keys (Gemini, OpenAI) stay on PurpleCallio servers. Audio is relayed through PurpleCallio; the browser never talks to a model provider directly.
- Verify the signature and timestamp on every tool request, and treat tool arguments as untrusted input from the model.
- Turn on "requires confirmation" for tools with side effects (refunds, bookings), so the user must approve each call in the browser.
- Keep secrets out of agent instructions and session metadata: both are sent to the model provider.
- Transcripts are stored only when you enable them on the agent, have card-like numbers, API keys and email addresses redacted, and are deleted after the retention period you choose.
Still having trouble?
If you're having an issue with this integration, our support team can help.
Contact SupportRelated documentation
Build with PurpleCallio
Explore the documentation to choose hosted UI, React components, or the headless SDK for your integration.