Code Examples
Copy-paste examples to get up and running quickly. Each example is self-contained and production-ready.
Voice Webhook Response Reference
When your webhook handles a voice channel event, the JSON response controls what the agent says and does. All fields are optional.
| Field | Type | Description |
|---|---|---|
text | string | Text the agent speaks to the caller |
hangup | boolean | End the call after speaking |
action | "transfer" | Cold-transfer the caller to the agent’s transferNumber |
// Normal response{ "text": "Your order shipped this morning!" }// End the call{ "text": "Goodbye!", "hangup": true }// Transfer to a human (requires transferNumber on the agent){ "text": "Let me connect you with our team.", "action": "transfer" }
For streaming responses, return Content-Type: application/x-ndjson — each line is a JSON object. Set "interim": true on non-final chunks so TTS starts immediately.
Browser Web Call
Start a voice call directly in the browser — no phone number needed. Create a web call via the API, then pass the access token to the AgentPhone Web SDK.
1. Create a web call (server-side)
const res = await fetch("https://api.agentphone.ai/v1/calls/web", {method: "POST",headers: {Authorization: `Bearer ${API_KEY}`,"Content-Type": "application/json",},body: JSON.stringify({agentId: "AGENT_ID",metadata: { userId: "usr_123" }, // optional}),});const { accessToken } = await res.json();// Send accessToken to your frontend
2. Install the Web SDK
npm install agentphone-web-sdk
3. Connect from the browser (client-side)
import { AgentPhoneWebClient } from "agentphone-web-sdk";const webClient = new AgentPhoneWebClient();// accessToken from your backend (valid for 30 seconds)await webClient.startCall({ accessToken });webClient.on("call_ended", () => {console.log("Call ended");});webClient.on("error", (error) => {console.error("Call error:", error);webClient.stopCall();});
Web calls use the same webhook flow as phone calls — your agent.message and agent.call_ended webhooks fire normally. The direction field will be "web" and fromNumber/toNumber will be "web" instead of E.164 numbers.
Live Transcript Streaming (SSE)
Stream a call’s transcript in real time using Server-Sent Events. The stream replays existing turns on connect, then delivers new turns as they happen. Works for both live and completed calls.
Node.js
const API_KEY = "YOUR_API_KEY";const CALL_ID = "call_abc123";const res = await fetch(`https://api.agentphone.ai/v1/calls/${CALL_ID}/transcript/stream`,{ headers: { Authorization: `Bearer ${API_KEY}` } });const reader = res.body.getReader();const decoder = new TextDecoder();let buffer = "";let eventType = null;while (true) {const { done, value } = await reader.read();if (done) break;buffer += decoder.decode(value, { stream: true });const lines = buffer.split("\n");buffer = lines.pop(); // keep incomplete line in bufferfor (const line of lines) {if (line.startsWith("event:")) {eventType = line.slice(6).trim();} else if (line.startsWith("data:")) {const data = JSON.parse(line.slice(5).trim());if (eventType === "connected") {console.log(`Streaming call ${data.callId} (${data.status})`);} else if (eventType === "turn") {console.log(`[${data.role}] ${data.content}`);} else if (eventType === "ended") {console.log(`Call ended — ${data.durationSeconds}s`);}}}}
Python
import jsonimport requestsAPI_KEY = "YOUR_API_KEY"CALL_ID = "call_abc123"url = f"https://api.agentphone.ai/v1/calls/{CALL_ID}/transcript/stream"headers = {"Authorization": f"Bearer {API_KEY}"}with requests.get(url, headers=headers, stream=True) as resp:resp.raise_for_status()event_type = Nonefor line in resp.iter_lines(decode_unicode=True):if not line:continueif line.startswith("event:"):event_type = line[len("event:"):].strip()elif line.startswith("data:"):data = json.loads(line[len("data:"):].strip())if event_type == "connected":print(f"Streaming call {data['callId']} ({data['status']})")elif event_type == "turn":print(f"[{data['role']}] {data['content']}")elif event_type == "ended":print(f"Call ended — {data['durationSeconds']}s")break
See the Calls guide for the full SSE event reference.
JavaScript / Node.js
A complete script that provisions a number, registers a webhook, and queries conversations and calls.
const API_KEY = "YOUR_API_KEY";const BASE_URL = "https://api.agentphone.ai";const headers = {Authorization: `Bearer ${API_KEY}`,"Content-Type": "application/json",};async function createNumber() {const res = await fetch(`${BASE_URL}/v1/numbers`, {method: "POST",headers,body: JSON.stringify({ country: "US" }),});return res.json();}async function registerWebhook(url) {const res = await fetch(`${BASE_URL}/v1/webhooks`, {method: "POST",headers,body: JSON.stringify({ url }),});return res.json();}async function listConversations() {const res = await fetch(`${BASE_URL}/v1/conversations`, { headers });return res.json();}async function listCalls() {const res = await fetch(`${BASE_URL}/v1/calls`, { headers });return res.json();}async function getCall(callId) {const res = await fetch(`${BASE_URL}/v1/calls/${callId}`, { headers });return res.json();}async function getCallTranscript(callId) {const res = await fetch(`${BASE_URL}/v1/calls/${callId}/transcript`, {headers,});return res.json();}async function makeOutboundCall(agentId, toNumber, fromNumberId) {const body = { agentId, toNumber };// Optional: pick which of the agent's numbers to call from.// If omitted, the agent's first assigned number is used.if (fromNumberId) body.fromNumberId = fromNumberId;const res = await fetch(`${BASE_URL}/v1/calls`, {method: "POST",headers,body: JSON.stringify(body),});return res.json();}// --- Usage ---const number = await createNumber();console.log(`Created: ${number.phoneNumber}`);const webhook = await registerWebhook("https://my-server.com/webhook");console.log(`Webhook secret: ${webhook.secret}`);const convos = await listConversations();console.log(`${convos.total} conversations`);const calls = await listCalls();console.log(`${calls.total} calls`);if (calls.data.length > 0) {const call = await getCall(calls.data[0].id);console.log(`Call transcripts: ${call.transcripts.length}`);const transcript = await getCallTranscript(calls.data[0].id);console.log(`Full transcript: ${transcript.transcript.length} turns`);}
Express.js Webhook Handler
Receives webhooks, verifies HMAC signatures, and routes SMS / voice events.
const express = require("express");const crypto = require("crypto");const app = express();const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;app.use("/webhook", express.raw({ type: "application/json" }));function verifyWebhook(payload, signature, timestamp, secret) {if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;const signedString = timestamp + "." + payload;const expected = crypto.createHmac("sha256", secret).update(signedString).digest("hex");return signature === `sha256=${expected}`;}app.post("/webhook", (req, res) => {const signature = req.headers["x-webhook-signature"];const timestamp = req.headers["x-webhook-timestamp"];if (!verifyWebhook(req.body, signature, timestamp, WEBHOOK_SECRET)) {return res.status(401).send("Invalid signature");}const payload = JSON.parse(req.body.toString());if (payload.event === "agent.message") {const { channel, data } = payload;if (channel === "sms") {console.log(`SMS from ${data.from}: ${data.message}`);processMessage(data).catch(console.error);return res.status(200).send("OK");}if (channel === "voice") {console.log(`Voice from ${data.from}: ${data.transcript}`);processVoice(data).then((response) => {res.status(200).json(response);}).catch(() => {res.status(200).json({ text: "Sorry, I encountered an error." });});return;}}if (payload.event === "agent.call_ended") {const { data } = payload;console.log(`Call ended: ${data.callId} (${data.durationSeconds}s, ${data.status})`);console.log(`Transcript: ${data.transcript.length} turns`);onCallEnded(data).catch(console.error);return res.status(200).send("OK");}res.status(200).send("OK");});async function processMessage(message) {// Your message processing logic (AI agent, database, queue, etc.)}async function processVoice(data) {// Return an object with text (and optionally hangup or action)// To transfer: return { text: "Connecting you now.", action: "transfer" }return { text: "Thanks for calling! How can I help?" };}async function onCallEnded(data) {// Trigger post-call tasks: send email, create CRM contact, generate summary, etc.// data.transcript contains the full conversation as [{role, content}, ...]// data.durationSeconds, data.summary, data.userSentiment are also available}app.listen(3000, () => console.log("Webhook server running on port 3000"));
Flask Webhook Handler
Python equivalent with HMAC verification and SMS/voice routing.
from flask import Flask, request, jsonifyimport hmacimport hashlibimport osimport timeapp = Flask(__name__)WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET")def verify_webhook(payload_body, signature, timestamp, secret):if abs(time.time() - int(timestamp)) > 300:return Falsesigned_string = f"{timestamp}.".encode() + payload_bodyexpected = hmac.new(secret.encode(), signed_string, hashlib.sha256).hexdigest()return hmac.compare_digest(f"sha256={expected}", signature)@app.route("/webhook", methods=["POST"])def webhook():signature = request.headers.get("X-Webhook-Signature")timestamp = request.headers.get("X-Webhook-Timestamp")if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):return jsonify({"error": "Invalid signature"}), 401payload = request.jsonif payload.get("event") == "agent.message":channel = payload.get("channel")data = payload.get("data", {})if channel == "sms":print(f"SMS from {data['from']}: {data['message']}")return jsonify({"status": "ok"}), 200if channel == "voice":transcript = data.get("transcript", "")response = get_ai_response(transcript)return jsonify(response), 200if payload.get("event") == "agent.call_ended":data = payload.get("data", {})print(f"Call ended: {data['callId']} ({data['durationSeconds']}s)")print(f"Transcript: {len(data.get('transcript', []))} turns")on_call_ended(data)return jsonify({"status": "ok"}), 200return jsonify({"status": "ok"}), 200def get_ai_response(transcript):# Return a dict with text (and optionally hangup or action)# To transfer: return {"text": "Connecting you now.", "action": "transfer"}return {"text": f"I heard you say: {transcript}. How can I help?"}def on_call_ended(data):# Trigger post-call tasks: send email, create CRM contact, generate summary, etc.# data["transcript"] contains the full conversation as [{"role": ..., "content": ...}, ...]# data["durationSeconds"], data["summary"], data["userSentiment"] are also availablepassif __name__ == "__main__":app.run(port=3000)
Voice Webhook with OpenAI
A voice-specific handler that pipes caller transcripts through GPT-4 and returns spoken responses.
from flask import Flask, request, jsonifyfrom openai import OpenAIimport osapp = Flask(__name__)client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))@app.route("/webhook", methods=["POST"])def webhook():payload = request.jsonif payload.get("event") == "agent.message":channel = payload.get("channel")data = payload.get("data", {})if channel == "voice":transcript = data.get("transcript", "")try:response = client.chat.completions.create(model="gpt-4",messages=[{"role": "system", "content": ("You are a helpful customer service assistant. ""If the caller asks to speak with a human, respond with TRANSFER_NOW.")},{"role": "user", "content": transcript},],max_tokens=150,)ai_text = response.choices[0].message.contentif "TRANSFER_NOW" in ai_text:return jsonify({"text": "Let me connect you with our team.", "action": "transfer"}), 200return jsonify({"text": ai_text}), 200except Exception as e:print(f"AI error: {e}")return jsonify({"text": "Sorry, I encountered an error."}), 200if channel == "sms":print(f"SMS from {data['from']}: {data['message']}")return jsonify({"status": "ok"}), 200return jsonify({"status": "ok"}), 200if __name__ == "__main__":app.run(port=3000)
Next.js API Route
Webhook handler as a Next.js Pages Router API route.
import crypto from "crypto";const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;function verifyWebhook(payload, signature, timestamp, secret) {if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;const signedString = timestamp + "." + payload;const expected = crypto.createHmac("sha256", secret).update(signedString).digest("hex");return signature === `sha256=${expected}`;}export default async function handler(req, res) {if (req.method !== "POST") {return res.status(405).json({ error: "Method not allowed" });}const signature = req.headers["x-webhook-signature"];const timestamp = req.headers["x-webhook-timestamp"];const rawBody = JSON.stringify(req.body);if (!verifyWebhook(rawBody, signature, timestamp, WEBHOOK_SECRET)) {return res.status(401).json({ error: "Invalid signature" });}const payload = req.body;if (payload.event === "agent.message") {const { channel, data } = payload;if (channel === "sms") {await processMessage(data);}if (channel === "voice") {const response = await processVoice(data);return res.status(200).json(response);}}if (payload.event === "agent.call_ended") {const { data } = payload;console.log(`Call ended: ${data.callId} (${data.durationSeconds}s)`);await onCallEnded(data);return res.status(200).json({ status: "ok" });}res.status(200).json({ status: "ok" });}async function processMessage(message) {console.log(`Processing message: ${message.body}`);}async function processVoice(data) {// Return an object with text (and optionally hangup or action)// To transfer: return { text: "Connecting you now.", action: "transfer" }return { text: "Thanks for calling! How can I help?" };}async function onCallEnded(data) {// data.transcript: full conversation [{role, content}, ...]// data.durationSeconds, data.summary, data.userSentiment also available}
Python API Client
A standalone script that provisions a number, registers a webhook, and lists conversations.
import requestsAPI_KEY = "YOUR_API_KEY"BASE_URL = "https://api.agentphone.ai"headers = {"Authorization": f"Bearer {API_KEY}"}# Create a phone numbernumber = requests.post(f"{BASE_URL}/v1/numbers",headers={**headers, "Content-Type": "application/json"},json={"country": "US"},).json()print(f"Created: {number['phoneNumber']}")# Register webhookwebhook = requests.post(f"{BASE_URL}/v1/webhooks",headers={**headers, "Content-Type": "application/json"},json={"url": "https://my-server.com/webhook"},).json()print(f"Webhook secret: {webhook['secret']}")# List conversationsconvos = requests.get(f"{BASE_URL}/v1/conversations", headers=headers).json()print(f"{convos['total']} conversations")# Get a specific conversationif convos["data"]:conv = requests.get(f"{BASE_URL}/v1/conversations/{convos['data'][0]['id']}",headers=headers,).json()print(f"Conversation with {conv['participant']}: {conv['messageCount']} messages")# Make an outbound call (optionally pick which number to call from)call = requests.post(f"{BASE_URL}/v1/calls",headers={**headers, "Content-Type": "application/json"},json={"agentId": "AGENT_ID","toNumber": "+14155551234",# "fromNumberId": "NUMBER_ID", # optional — defaults to agent's first number},).json()print(f"Call {call['id']}: {call['fromNumber']} -> {call['toNumber']}")# List callscalls = requests.get(f"{BASE_URL}/v1/calls", headers=headers).json()print(f"{calls['total']} calls")# Fetch full transcript for a completed callif calls["data"]:transcript = requests.get(f"{BASE_URL}/v1/calls/{calls['data'][0]['id']}/transcript",headers=headers,).json()for turn in transcript["transcript"]:print(f" [{turn['role']}] {turn['content']}")

