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.

FieldTypeDescription
textstringText the agent speaks to the caller
hangupbooleanEnd 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)

create-web-call.js
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)

web-call-client.js
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

stream-transcript.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 buffer
for (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

stream_transcript.py
import json
import requests
API_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 = None
for line in resp.iter_lines(decode_unicode=True):
if not line:
continue
if 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.

sms-api.js
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.

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

webhook.py
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import time
app = 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 False
signed_string = f"{timestamp}.".encode() + payload_body
expected = 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"}), 401
payload = request.json
if 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"}), 200
if channel == "voice":
transcript = data.get("transcript", "")
response = get_ai_response(transcript)
return jsonify(response), 200
if 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"}), 200
return jsonify({"status": "ok"}), 200
def 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 available
pass
if __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.

voice_webhook.py
from flask import Flask, request, jsonify
from openai import OpenAI
import os
app = Flask(__name__)
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
@app.route("/webhook", methods=["POST"])
def webhook():
payload = request.json
if 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.content
if "TRANSFER_NOW" in ai_text:
return jsonify({"text": "Let me connect you with our team.", "action": "transfer"}), 200
return jsonify({"text": ai_text}), 200
except Exception as e:
print(f"AI error: {e}")
return jsonify({"text": "Sorry, I encountered an error."}), 200
if channel == "sms":
print(f"SMS from {data['from']}: {data['message']}")
return jsonify({"status": "ok"}), 200
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(port=3000)

Next.js API Route

Webhook handler as a Next.js Pages Router API route.

pages/api/webhook.js
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.

sms_api.py
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.agentphone.ai"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Create a phone number
number = requests.post(
f"{BASE_URL}/v1/numbers",
headers={**headers, "Content-Type": "application/json"},
json={"country": "US"},
).json()
print(f"Created: {number['phoneNumber']}")
# Register webhook
webhook = 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 conversations
convos = requests.get(f"{BASE_URL}/v1/conversations", headers=headers).json()
print(f"{convos['total']} conversations")
# Get a specific conversation
if 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 calls
calls = requests.get(f"{BASE_URL}/v1/calls", headers=headers).json()
print(f"{calls['total']} calls")
# Fetch full transcript for a completed call
if 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']}")