Skip to navigation

Code Examples

These starter examples cover browser calls and signed webhook handlers. Add durable processing and application-specific error handling before production.

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 returned connection details to the AgentPhone Web SDK (version 0.2.0 or later).

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 ${process.env.AGENTPHONE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: "AGENT_ID",
metadata: { userId: "usr_123" }, // optional
}),
});
if (!res.ok) throw new Error(`Web call failed: ${res.status} ${await res.text()}`);
const { accessToken, callId, transport, iceServers, expiresAt } = await res.json();
// Send these connection details to your frontend. The token is short-lived:
// connect before expiresAt (Unix epoch milliseconds).

The response also includes id — the AgentPhone call ID for use with the call APIs (transcripts, recordings, ending the call). callId identifies the media connection and is not interchangeable with id.

2. Install the Web SDK

npm install agentphone-web-sdk

Use version 0.2.0 or later — earlier versions only pass the access token and cannot establish current connections.

3. Connect from the browser (client-side)

web-call-client.js
import { AgentPhoneWebClient } from "agentphone-web-sdk";
const webClient = new AgentPhoneWebClient();
webClient.on("call_ended", () => {
console.log("Call ended");
});
webClient.on("error", (error) => {
console.error("Call error:", error);
webClient.stopCall();
});
// Register listeners first, then use the fresh connection details from your backend.
await webClient.startCall({ accessToken, callId, transport, iceServers });

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 events are not delivered over the media connection — fetch transcripts through the call API or receive them via webhooks, the same as phone calls.

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

The examples below use environment variables for credentials. Copy verify-webhook.mjs from Webhook security into the same directory. It verifies raw bytes before JSON parsing.

Express.js Webhook Handler

Install Express with npm install express. Save the following as server.mjs, set AGENTPHONE_WEBHOOK_SECRET, and run node server.mjs.

server.mjs
import express from "express";
import { verifyWebhook } from "./verify-webhook.mjs";
const secret = process.env.AGENTPHONE_WEBHOOK_SECRET;
if (!secret) throw new Error("Set AGENTPHONE_WEBHOOK_SECRET");
const app = express();
// Mount before any express.json() middleware so the signed bytes stay intact.
app.post("/webhook", express.raw({ type: "application/json", limit: "2mb" }), (req, res) => {
if (!verifyWebhook(req.body, req.get("X-Webhook-Signature"), req.get("X-Webhook-Timestamp"), secret)) {
return res.sendStatus(401);
}
let event;
try { event = JSON.parse(req.body.toString("utf8")); }
catch { return res.sendStatus(400); }
if (!event || typeof event !== "object" || Array.isArray(event)) return res.sendStatus(400);
if (event.event === "agent.message" && event.channel === "voice") {
// Demo response: replace with your own logic using data.transcript.
return res.json({ text: `You said: ${event.data?.transcript ?? "hello"}` });
}
// Text: acknowledge only. A response body does not send an SMS.
// Enqueue and deduplicate real work durably before returning 2xx.
return res.sendStatus(200);
});
app.listen(3000);

Flask Webhook Handler

Copy verify_webhook.py from Webhook security, then install Flask with pip install flask. Set AGENTPHONE_WEBHOOK_SECRET and run flask --app server run --port 3000.

server.py
import os
from flask import Flask, jsonify, request
from verify_webhook import verify_webhook
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024
secret = os.environ["AGENTPHONE_WEBHOOK_SECRET"]
@app.post("/webhook")
def webhook():
raw_body = request.get_data()
if not verify_webhook(
raw_body, request.headers.get("X-Webhook-Signature"),
request.headers.get("X-Webhook-Timestamp"), secret,
):
return "Invalid signature", 401
event = request.get_json(silent=True)
if not isinstance(event, dict):
return "Invalid JSON object", 400
if event.get("event") == "agent.message" and event.get("channel") == "voice":
transcript = event.get("data", {}).get("transcript", "hello")
return jsonify({"text": f"You said: {transcript}"})
# Enqueue and deduplicate real text processing before acknowledging.
return "OK", 200

Voice Webhook with OpenAI

When connecting any LLM to a voice handler, build the conversation from recentHistory and the current data.transcript. History is context; do not assume it contains the latest caller turn. Keep model credentials on the server and choose a model available to your account.

Verify the webhook before starting a response. For slow tool calls, return Content-Type: application/x-ndjson and send an interim spoken acknowledgement, then a final text chunk. Bound tool execution and handle errors with a spoken fallback. The voice timeout still applies between reads; one interim chunk does not allow an unlimited wait. See streaming voice responses.

Next.js API Route

For the App Router, request.arrayBuffer() preserves the raw body. Place this in app/api/webhook/route.js and copy the verifier to lib/verify-webhook.mjs:

app/api/webhook/route.js
import { verifyWebhook } from "../../../lib/verify-webhook.mjs";
export const runtime = "nodejs";
export async function POST(request) {
const raw = Buffer.from(await request.arrayBuffer());
if (!verifyWebhook(raw, request.headers.get("X-Webhook-Signature"),
request.headers.get("X-Webhook-Timestamp"), process.env.AGENTPHONE_WEBHOOK_SECRET)) {
return new Response("Invalid signature", { status: 401 });
}
let event;
try { event = JSON.parse(raw.toString("utf8")); }
catch { return new Response("Invalid JSON", { status: 400 }); }
if (!event || typeof event !== "object" || Array.isArray(event)) {
return new Response("Expected object", { status: 400 });
}
if (event.event === "agent.message" && event.channel === "voice") {
return Response.json({ text: `You said: ${event.data?.transcript ?? "hello"}` });
}
// Enqueue and deduplicate text work before acknowledging.
return new Response(null, { status: 200 });
}

Do not use JSON.stringify() on a parsed request to verify the signature. In the Pages Router, disable its body parser and read the original stream instead. Configure an appropriate request-size limit in your hosting layer.

Python API Client

Use the Python SDK for supported methods or authenticated HTTP for endpoints it does not expose. See Authentication and Sub-accounts for account scoping.