Voice API
Make outbound voice calls — plain or AI-powered — with a single endpoint.
Outbound voice calls route through SIP to the carrier and out to the PSTN. A plain call only needs from and to. Add a systemPrompt to make it an AI-powered conversation.
There is no agent_id field. AI calls are triggered by including systemPrompt in the request body. Without it, you get a plain outbound call.
Create call
POST /api/voice
Initiate an outbound call. Include systemPrompt to make it an AI call, or omit it for a plain call.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Caller phone number in E.164 format |
to | string | Yes | Destination phone number in E.164 format |
systemPrompt | string | No | AI agent system prompt. Including this triggers an AI-powered call. |
model | string | No | Ultravox model (e.g. "fixie-ai/ultravox-70B") |
voice | string | No | Voice ID for TTS (e.g. "Mark") |
tools | array | No | HTTP tools the AI can call mid-conversation (see below) |
firstSpeaker | string | No | Who speaks first: "agent" or "user" (default: "agent") |
temperature | number | No | LLM temperature, 0 to 1 |
maxDuration | string | No | Max call duration (e.g. "300s") |
metadata | object | No | Custom key-value metadata (string values only) |
Tool schema (for tools array)
Each tool in the tools array has the following shape:
| Field | Type | Required | Description |
|---|---|---|---|
modelToolName | string | Yes | Name the AI uses to invoke this tool |
description | string | Yes | What the tool does (shown to the AI) |
dynamicParameters | array | No | Parameters the AI can pass |
http | object | No | HTTP endpoint (baseUrlPattern, httpMethod) |
Plain call example
curl -X POST https://api.trunx.io/api/voice \
-H "Authorization: Bearer tk_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "+14155559876",
"to": "+14155551234"
}'const res = await fetch("https://api.trunx.io/api/voice", {
method: "POST",
headers: {
"Authorization": "Bearer tk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "+14155559876",
to: "+14155551234",
}),
});
const data = await res.json();import requests
res = requests.post(
"https://api.trunx.io/api/voice",
headers={"Authorization": "Bearer tk_live_..."},
json={
"from": "+14155559876",
"to": "+14155551234",
},
)
data = res.json()AI call example
curl -X POST https://api.trunx.io/api/voice \
-H "Authorization: Bearer tk_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "+14155559876",
"to": "+14155551234",
"systemPrompt": "You are calling to confirm a dental appointment for tomorrow at 2pm. Be friendly and professional.",
"voice": "Mark",
"firstSpeaker": "agent",
"temperature": 0.7,
"maxDuration": "300s"
}'const res = await fetch("https://api.trunx.io/api/voice", {
method: "POST",
headers: {
"Authorization": "Bearer tk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "+14155559876",
to: "+14155551234",
systemPrompt:
"You are calling to confirm a dental appointment for tomorrow at 2pm. Be friendly and professional.",
voice: "Mark",
firstSpeaker: "agent",
temperature: 0.7,
maxDuration: "300s",
}),
});
const data = await res.json();import requests
res = requests.post(
"https://api.trunx.io/api/voice",
headers={"Authorization": "Bearer tk_live_..."},
json={
"from": "+14155559876",
"to": "+14155551234",
"systemPrompt": "You are calling to confirm a dental appointment for tomorrow at 2pm. Be friendly and professional.",
"voice": "Mark",
"firstSpeaker": "agent",
"temperature": 0.7,
"maxDuration": "300s",
},
)
data = res.json()Response
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"callId": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "initiated"
}Get call details
GET /api/voice/{id}
Retrieve call details including status, duration, and AI transcript (if applicable).
curl "https://api.trunx.io/api/voice/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer tk_live_..."const res = await fetch(
"https://api.trunx.io/api/voice/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
{ headers: { "Authorization": "Bearer tk_live_..." } }
);
const data = await res.json();import requests
res = requests.get(
"https://api.trunx.io/api/voice/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
headers={"Authorization": "Bearer tk_live_..."},
)
data = res.json()Response (plain call)
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"from": "+14155559876",
"to": "+14155551234",
"status": "completed",
"provider": "twilio",
"direction": "outbound",
"duration": 45,
"createdAt": "2026-03-09T15:30:00.000Z"
}Response (AI call)
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"from": "+14155559876",
"to": "+14155551234",
"status": "completed",
"provider": "ultravox",
"direction": "outbound",
"duration": 142,
"createdAt": "2026-03-09T15:30:00.000Z",
"ai": {
"status": "ended",
"transcript": "Agent: Hi, this is Sarah calling from Downtown Dental...\nUser: Yes, this is John...",
"duration": 140,
"endReason": "hangup"
}
}The ai field is only present for AI-powered calls (those created with systemPrompt). Returns 404 if the call ID does not exist.
End call
DELETE /api/voice/{id}
Hang up an active call.
curl -X DELETE "https://api.trunx.io/api/voice/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer tk_live_..."const res = await fetch(
"https://api.trunx.io/api/voice/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
{
method: "DELETE",
headers: { "Authorization": "Bearer tk_live_..." },
}
);
const data = await res.json();import requests
res = requests.delete(
"https://api.trunx.io/api/voice/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
headers={"Authorization": "Bearer tk_live_..."},
)
data = res.json()Response
{
"ended": true
}Returns 404 if the call ID does not exist.
Call statuses
| Status | Description |
|---|---|
initiated | Call accepted and being placed |
ringing | Call is ringing at the destination |
answered | Call connected |
completed | Call ended normally |
failed | Call could not be completed |
For AI calls, the ai.transcript and ai.endReason fields are available after the call ends. Poll the get call endpoint or configure a webhook for real-time updates.