> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://agentphone.to/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://agentphone.to/docs/_mcp/server.

# Code Examples

> End-to-end examples for common AgentPhone integrations.

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.

| 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` |

```text
// 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](https://www.npmjs.com/package/agentphone-web-sdk) (version 0.2.0 or later).

### 1. Create a web call (server-side)

**`create-web-call.js`**

```javascript title="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

```bash
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`**

```javascript title="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](/docs/documentation/guides/calls) 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`**

```javascript title="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`**

```python title="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](/docs/documentation/guides/calls#stream-transcript-sse) for the full SSE event reference.

## JavaScript / Node.js

The examples below use environment variables for credentials. Copy `verify-webhook.mjs` from [Webhook security](/docs/documentation/guides/webhooks#verification-example-nodejs) 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`**

```javascript title="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](/docs/documentation/guides/webhooks#verification-example-python), then install Flask with `pip install flask`. Set `AGENTPHONE_WEBHOOK_SECRET` and run `flask --app server run --port 3000`.

**`server.py`**

```python title="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](/docs/documentation/guides/calls#streaming-response-recommended).

## 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`**

```javascript title="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](/docs/documentation/sdks/python) for supported methods or authenticated HTTP for endpoints it does not expose. See [Authentication](/docs/authentication) and [Sub-accounts](/docs/sub-accounts) for account scoping.