PurpleCallio

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.
parameters.json
{
  "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.

server.ts
// 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.

agent-call.ts
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.

SupportAgent.tsx
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>
  );
}
SupportWidget.tsx
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.

request.json
{
  "tool": "orderStatus",
  "arguments": { "orderId": "ord_123" },
  "sessionId": "…",
  "callId": "…",
  "agentId": "…",
  "participant": { "id": "user_42", "name": "Sam" }
}
HeaderValue
X-PurpleCallio-Eventai.tool_call
X-PurpleCallio-TimestampUnix time in seconds when the request was signed
X-PurpleCallio-Signaturesha256=<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.

tools.js
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.

CodeMeaning
AI_DISABLEDAI voice agents are not enabled on this deployment.
AI_PAID_PLAN_REQUIREDAI voice agents unlock after your first verified credit purchase.
AI_AGENT_NOT_FOUNDThe agent id does not exist or belongs to another account.
AI_AGENT_DISABLEDThe agent is disabled in the dashboard.
AI_QUOTA_EXCEEDEDThe daily or monthly AI minute limit is used up.
AI_CONCURRENCY_LIMITToo many AI calls are running for the account or platform. Retry shortly.
AI_INSUFFICIENT_CREDITSThe credit balance cannot cover a call. Top up or renew.
AI_SESSION_CREATE_FAILEDThe session could not be created. Retry with backoff.
AI_PROVIDER_UNAVAILABLEThe voice model provider is unreachable. The session is not billed.
AI_CONNECTION_FAILEDThe live connection to the agent failed.
AI_SESSION_EXPIREDThe token was not used within its time to connect (5 minutes by default).
AI_SESSION_ENDEDThe session already ended. Create a new one; tokens are single-use.
AI_MAX_DURATION_REACHEDThe call hit the agent’s maximum duration and was ended.
AI_TOOL_FAILEDA tool endpoint failed, was refused or returned an invalid response.
AI_TOOL_TIMEOUTA tool endpoint did not answer within its timeout.
AI_MICROPHONE_DENIEDBrowser SDK only: the user denied microphone access.
AI_MICROPHONE_UNAVAILABLEBrowser SDK only: no microphone was found.
AI_AUDIO_UNSUPPORTEDBrowser SDK only: the browser lacks the audio APIs the call needs.

Limits

Default limits; contact support if you need higher ones.

LimitDefault
Concurrent AI calls per account3
Platform-wide concurrent AI calls50
AI minutes per account per day120
AI minutes per account per month1,000
Maximum call duration30 minutes (agent default 10 minutes, minimum 30 seconds)
Time to connect after creating a session5 minutes
Tool calls per session20
Tool timeout8 seconds
Tool response size64 KB
Stored transcript messages per call500

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 Support

Related documentation

Build with PurpleCallio

Explore the documentation to choose hosted UI, React components, or the headless SDK for your integration.