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
1// Normal response
2{ "text": "Your order shipped this morning!" }
3
4// End the call
5{ "text": "Goodbye!", "hangup": true }
6
7// Transfer to a human (requires transferNumber on the agent)
8{ "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
1const res = await fetch("https://api.agentphone.to/v1/calls/web", {
2 method: "POST",
3 headers: {
4 Authorization: `Bearer ${API_KEY}`,
5 "Content-Type": "application/json",
6 },
7 body: JSON.stringify({
8 agentId: "AGENT_ID",
9 metadata: { userId: "usr_123" }, // optional
10 }),
11});
12const { accessToken } = await res.json();
13// 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
1import { AgentPhoneWebClient } from "agentphone-web-sdk";
2
3const webClient = new AgentPhoneWebClient();
4
5// accessToken from your backend (valid for 30 seconds)
6await webClient.startCall({ accessToken });
7
8webClient.on("call_ended", () => {
9 console.log("Call ended");
10});
11
12webClient.on("error", (error) => {
13 console.error("Call error:", error);
14 webClient.stopCall();
15});

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
1const API_KEY = "YOUR_API_KEY";
2const CALL_ID = "call_abc123";
3
4const res = await fetch(
5 `https://api.agentphone.to/v1/calls/${CALL_ID}/transcript/stream`,
6 { headers: { Authorization: `Bearer ${API_KEY}` } }
7);
8
9const reader = res.body.getReader();
10const decoder = new TextDecoder();
11let buffer = "";
12let eventType = null;
13
14while (true) {
15 const { done, value } = await reader.read();
16 if (done) break;
17
18 buffer += decoder.decode(value, { stream: true });
19 const lines = buffer.split("\n");
20 buffer = lines.pop(); // keep incomplete line in buffer
21
22 for (const line of lines) {
23 if (line.startsWith("event:")) {
24 eventType = line.slice(6).trim();
25 } else if (line.startsWith("data:")) {
26 const data = JSON.parse(line.slice(5).trim());
27
28 if (eventType === "connected") {
29 console.log(`Streaming call ${data.callId} (${data.status})`);
30 } else if (eventType === "turn") {
31 console.log(`[${data.role}] ${data.content}`);
32 } else if (eventType === "ended") {
33 console.log(`Call ended — ${data.durationSeconds}s`);
34 }
35 }
36 }
37}

Python

stream_transcript.py
1import json
2import requests
3
4API_KEY = "YOUR_API_KEY"
5CALL_ID = "call_abc123"
6
7url = f"https://api.agentphone.to/v1/calls/{CALL_ID}/transcript/stream"
8headers = {"Authorization": f"Bearer {API_KEY}"}
9
10with requests.get(url, headers=headers, stream=True) as resp:
11 resp.raise_for_status()
12 event_type = None
13 for line in resp.iter_lines(decode_unicode=True):
14 if not line:
15 continue
16 if line.startswith("event:"):
17 event_type = line[len("event:"):].strip()
18 elif line.startswith("data:"):
19 data = json.loads(line[len("data:"):].strip())
20 if event_type == "connected":
21 print(f"Streaming call {data['callId']} ({data['status']})")
22 elif event_type == "turn":
23 print(f"[{data['role']}] {data['content']}")
24 elif event_type == "ended":
25 print(f"Call ended — {data['durationSeconds']}s")
26 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
1const API_KEY = "YOUR_API_KEY";
2const BASE_URL = "https://api.agentphone.to";
3
4const headers = {
5 Authorization: `Bearer ${API_KEY}`,
6 "Content-Type": "application/json",
7};
8
9async function createNumber() {
10 const res = await fetch(`${BASE_URL}/v1/numbers`, {
11 method: "POST",
12 headers,
13 body: JSON.stringify({ country: "US" }),
14 });
15 return res.json();
16}
17
18async function registerWebhook(url) {
19 const res = await fetch(`${BASE_URL}/v1/webhooks`, {
20 method: "POST",
21 headers,
22 body: JSON.stringify({ url }),
23 });
24 return res.json();
25}
26
27async function listConversations() {
28 const res = await fetch(`${BASE_URL}/v1/conversations`, { headers });
29 return res.json();
30}
31
32async function listCalls() {
33 const res = await fetch(`${BASE_URL}/v1/calls`, { headers });
34 return res.json();
35}
36
37async function getCall(callId) {
38 const res = await fetch(`${BASE_URL}/v1/calls/${callId}`, { headers });
39 return res.json();
40}
41
42async function getCallTranscript(callId) {
43 const res = await fetch(`${BASE_URL}/v1/calls/${callId}/transcript`, {
44 headers,
45 });
46 return res.json();
47}
48
49async function makeOutboundCall(agentId, toNumber, fromNumberId) {
50 const body = { agentId, toNumber };
51 // Optional: pick which of the agent's numbers to call from.
52 // If omitted, the agent's first assigned number is used.
53 if (fromNumberId) body.fromNumberId = fromNumberId;
54
55 const res = await fetch(`${BASE_URL}/v1/calls`, {
56 method: "POST",
57 headers,
58 body: JSON.stringify(body),
59 });
60 return res.json();
61}
62
63// --- Usage ---
64const number = await createNumber();
65console.log(`Created: ${number.phoneNumber}`);
66
67const webhook = await registerWebhook("https://my-server.com/webhook");
68console.log(`Webhook secret: ${webhook.secret}`);
69
70const convos = await listConversations();
71console.log(`${convos.total} conversations`);
72
73const calls = await listCalls();
74console.log(`${calls.total} calls`);
75
76if (calls.data.length > 0) {
77 const call = await getCall(calls.data[0].id);
78 console.log(`Call transcripts: ${call.transcripts.length}`);
79
80 const transcript = await getCallTranscript(calls.data[0].id);
81 console.log(`Full transcript: ${transcript.transcript.length} turns`);
82}

Express.js Webhook Handler

Receives webhooks, verifies HMAC signatures, and routes SMS / voice events.

webhook.js
1const express = require("express");
2const crypto = require("crypto");
3const app = express();
4
5const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
6
7app.use("/webhook", express.raw({ type: "application/json" }));
8
9function verifyWebhook(payload, signature, timestamp, secret) {
10 if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
11 const signedString = timestamp + "." + payload;
12 const expected = crypto
13 .createHmac("sha256", secret)
14 .update(signedString)
15 .digest("hex");
16 return signature === `sha256=${expected}`;
17}
18
19app.post("/webhook", (req, res) => {
20 const signature = req.headers["x-webhook-signature"];
21 const timestamp = req.headers["x-webhook-timestamp"];
22
23 if (!verifyWebhook(req.body, signature, timestamp, WEBHOOK_SECRET)) {
24 return res.status(401).send("Invalid signature");
25 }
26
27 const payload = JSON.parse(req.body.toString());
28
29 if (payload.event === "agent.message") {
30 const { channel, data } = payload;
31
32 if (channel === "sms") {
33 console.log(`SMS from ${data.from}: ${data.message}`);
34 processMessage(data).catch(console.error);
35 return res.status(200).send("OK");
36 }
37
38 if (channel === "voice") {
39 console.log(`Voice from ${data.from}: ${data.transcript}`);
40 processVoice(data)
41 .then((response) => {
42 res.status(200).json(response);
43 })
44 .catch(() => {
45 res.status(200).json({ text: "Sorry, I encountered an error." });
46 });
47 return;
48 }
49 }
50
51 if (payload.event === "agent.call_ended") {
52 const { data } = payload;
53 console.log(
54 `Call ended: ${data.callId} (${data.durationSeconds}s, ${data.status})`
55 );
56 console.log(`Transcript: ${data.transcript.length} turns`);
57 onCallEnded(data).catch(console.error);
58 return res.status(200).send("OK");
59 }
60
61 res.status(200).send("OK");
62});
63
64async function processMessage(message) {
65 // Your message processing logic (AI agent, database, queue, etc.)
66}
67
68async function processVoice(data) {
69 // Return an object with text (and optionally hangup or action)
70 // To transfer: return { text: "Connecting you now.", action: "transfer" }
71 return { text: "Thanks for calling! How can I help?" };
72}
73
74async function onCallEnded(data) {
75 // Trigger post-call tasks: send email, create CRM contact, generate summary, etc.
76 // data.transcript contains the full conversation as [{role, content}, ...]
77 // data.durationSeconds, data.summary, data.userSentiment are also available
78}
79
80app.listen(3000, () => console.log("Webhook server running on port 3000"));

Flask Webhook Handler

Python equivalent with HMAC verification and SMS/voice routing.

webhook.py
1from flask import Flask, request, jsonify
2import hmac
3import hashlib
4import os
5import time
6
7app = Flask(__name__)
8WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET")
9
10
11def verify_webhook(payload_body, signature, timestamp, secret):
12 if abs(time.time() - int(timestamp)) > 300:
13 return False
14 signed_string = f"{timestamp}.".encode() + payload_body
15 expected = hmac.new(
16 secret.encode(), signed_string, hashlib.sha256
17 ).hexdigest()
18 return hmac.compare_digest(f"sha256={expected}", signature)
19
20
21@app.route("/webhook", methods=["POST"])
22def webhook():
23 signature = request.headers.get("X-Webhook-Signature")
24 timestamp = request.headers.get("X-Webhook-Timestamp")
25
26 if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):
27 return jsonify({"error": "Invalid signature"}), 401
28
29 payload = request.json
30
31 if payload.get("event") == "agent.message":
32 channel = payload.get("channel")
33 data = payload.get("data", {})
34
35 if channel == "sms":
36 print(f"SMS from {data['from']}: {data['message']}")
37 return jsonify({"status": "ok"}), 200
38
39 if channel == "voice":
40 transcript = data.get("transcript", "")
41 response = get_ai_response(transcript)
42 return jsonify(response), 200
43
44 if payload.get("event") == "agent.call_ended":
45 data = payload.get("data", {})
46 print(f"Call ended: {data['callId']} ({data['durationSeconds']}s)")
47 print(f"Transcript: {len(data.get('transcript', []))} turns")
48 on_call_ended(data)
49 return jsonify({"status": "ok"}), 200
50
51 return jsonify({"status": "ok"}), 200
52
53
54def get_ai_response(transcript):
55 # Return a dict with text (and optionally hangup or action)
56 # To transfer: return {"text": "Connecting you now.", "action": "transfer"}
57 return {"text": f"I heard you say: {transcript}. How can I help?"}
58
59
60def on_call_ended(data):
61 # Trigger post-call tasks: send email, create CRM contact, generate summary, etc.
62 # data["transcript"] contains the full conversation as [{"role": ..., "content": ...}, ...]
63 # data["durationSeconds"], data["summary"], data["userSentiment"] are also available
64 pass
65
66
67if __name__ == "__main__":
68 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
1from flask import Flask, request, jsonify
2from openai import OpenAI
3import os
4
5app = Flask(__name__)
6client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
7
8
9@app.route("/webhook", methods=["POST"])
10def webhook():
11 payload = request.json
12
13 if payload.get("event") == "agent.message":
14 channel = payload.get("channel")
15 data = payload.get("data", {})
16
17 if channel == "voice":
18 transcript = data.get("transcript", "")
19 try:
20 response = client.chat.completions.create(
21 model="gpt-4",
22 messages=[
23 {"role": "system", "content": (
24 "You are a helpful customer service assistant. "
25 "If the caller asks to speak with a human, respond with TRANSFER_NOW."
26 )},
27 {"role": "user", "content": transcript},
28 ],
29 max_tokens=150,
30 )
31 ai_text = response.choices[0].message.content
32 if "TRANSFER_NOW" in ai_text:
33 return jsonify({"text": "Let me connect you with our team.", "action": "transfer"}), 200
34 return jsonify({"text": ai_text}), 200
35 except Exception as e:
36 print(f"AI error: {e}")
37 return jsonify({"text": "Sorry, I encountered an error."}), 200
38
39 if channel == "sms":
40 print(f"SMS from {data['from']}: {data['message']}")
41 return jsonify({"status": "ok"}), 200
42
43 return jsonify({"status": "ok"}), 200
44
45
46if __name__ == "__main__":
47 app.run(port=3000)

Next.js API Route

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

pages/api/webhook.js
1import crypto from "crypto";
2
3const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
4
5function verifyWebhook(payload, signature, timestamp, secret) {
6 if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
7 const signedString = timestamp + "." + payload;
8 const expected = crypto
9 .createHmac("sha256", secret)
10 .update(signedString)
11 .digest("hex");
12 return signature === `sha256=${expected}`;
13}
14
15export default async function handler(req, res) {
16 if (req.method !== "POST") {
17 return res.status(405).json({ error: "Method not allowed" });
18 }
19
20 const signature = req.headers["x-webhook-signature"];
21 const timestamp = req.headers["x-webhook-timestamp"];
22 const rawBody = JSON.stringify(req.body);
23
24 if (!verifyWebhook(rawBody, signature, timestamp, WEBHOOK_SECRET)) {
25 return res.status(401).json({ error: "Invalid signature" });
26 }
27
28 const payload = req.body;
29
30 if (payload.event === "agent.message") {
31 const { channel, data } = payload;
32
33 if (channel === "sms") {
34 await processMessage(data);
35 }
36
37 if (channel === "voice") {
38 const response = await processVoice(data);
39 return res.status(200).json(response);
40 }
41 }
42
43 if (payload.event === "agent.call_ended") {
44 const { data } = payload;
45 console.log(`Call ended: ${data.callId} (${data.durationSeconds}s)`);
46 await onCallEnded(data);
47 return res.status(200).json({ status: "ok" });
48 }
49
50 res.status(200).json({ status: "ok" });
51}
52
53async function processMessage(message) {
54 console.log(`Processing message: ${message.body}`);
55}
56
57async function processVoice(data) {
58 // Return an object with text (and optionally hangup or action)
59 // To transfer: return { text: "Connecting you now.", action: "transfer" }
60 return { text: "Thanks for calling! How can I help?" };
61}
62
63async function onCallEnded(data) {
64 // data.transcript: full conversation [{role, content}, ...]
65 // data.durationSeconds, data.summary, data.userSentiment also available
66}

Python API Client

A standalone script that provisions a number, registers a webhook, and lists conversations.

sms_api.py
1import requests
2
3API_KEY = "YOUR_API_KEY"
4BASE_URL = "https://api.agentphone.to"
5headers = {"Authorization": f"Bearer {API_KEY}"}
6
7# Create a phone number
8number = requests.post(
9 f"{BASE_URL}/v1/numbers",
10 headers={**headers, "Content-Type": "application/json"},
11 json={"country": "US"},
12).json()
13print(f"Created: {number['phoneNumber']}")
14
15# Register webhook
16webhook = requests.post(
17 f"{BASE_URL}/v1/webhooks",
18 headers={**headers, "Content-Type": "application/json"},
19 json={"url": "https://my-server.com/webhook"},
20).json()
21print(f"Webhook secret: {webhook['secret']}")
22
23# List conversations
24convos = requests.get(f"{BASE_URL}/v1/conversations", headers=headers).json()
25print(f"{convos['total']} conversations")
26
27# Get a specific conversation
28if convos["data"]:
29 conv = requests.get(
30 f"{BASE_URL}/v1/conversations/{convos['data'][0]['id']}",
31 headers=headers,
32 ).json()
33 print(f"Conversation with {conv['participant']}: {conv['messageCount']} messages")
34
35# Make an outbound call (optionally pick which number to call from)
36call = requests.post(
37 f"{BASE_URL}/v1/calls",
38 headers={**headers, "Content-Type": "application/json"},
39 json={
40 "agentId": "AGENT_ID",
41 "toNumber": "+14155551234",
42 # "fromNumberId": "NUMBER_ID", # optional — defaults to agent's first number
43 },
44).json()
45print(f"Call {call['id']}: {call['fromNumber']} -> {call['toNumber']}")
46
47# List calls
48calls = requests.get(f"{BASE_URL}/v1/calls", headers=headers).json()
49print(f"{calls['total']} calls")
50
51# Fetch full transcript for a completed call
52if calls["data"]:
53 transcript = requests.get(
54 f"{BASE_URL}/v1/calls/{calls['data'][0]['id']}/transcript",
55 headers=headers,
56 ).json()
57 for turn in transcript["transcript"]:
58 print(f" [{turn['role']}] {turn['content']}")