AssistantFleet API
Build voice assistants programmatically. Create an assistant, point a phone number at it, and your callers talk to OpenAI Realtime in real time. Every call is logged, costed, and (optionally) forwarded to your backend via signed webhooks.
Quick start
- Sign in at assistant.voicefleet.ai with Google. Your tenant is provisioned on first login.
-
Create an API key in the dashboard under API keys. Copy it once β it starts with
af_live_. -
Create an assistant:
curl -X POST https://assistant.voicefleet.ai/assistants \ -H "Authorization: Bearer af_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Front desk", "instructions": "You are a friendly receptionist for Acme Clinic. Answer questions about hours and bookings.", "first_message": "Thanks for calling Acme, how can I help?", "model": "gpt-realtime-2.1", "voice": "marin" }' -
Bind a phone number on a supported provider (Telnyx, Twilio, Voximplant, VoIPCloud). E.164, no spaces:
curl -X PUT https://assistant.voicefleet.ai/numbers/+13125550100 \ -H "Authorization: Bearer af_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"assistant_id": "<assistant-id-from-step-3>", "provider": "telnyx"}'providerdefaults totelnyx. See Telecom providers for the per-provider webhook URL and setup. -
Point your provider at us. Configure your number's voice webhook URL β depending on provider:
- Telnyx β
https://assistant.voicefleet.ai/telnyx/route - Twilio β
https://assistant.voicefleet.ai/twilio/route - Voximplant β
https://assistant.voicefleet.ai/voximplant/route(queried from a VoxEngine scenario) - VoIPCloud β
https://assistant.voicefleet.ai/voipcloud/route(PBX queries for forward target)
- Telnyx β
Don't want to set up Telnyx yet? Use the Test in browser button in the dashboard to talk to your assistant from the browser with your microphone β the call is logged the same way as a phone call.
Authentication
Every request to a tenant-scoped endpoint must include a Bearer token:
Authorization: Bearer af_live_β¦
Keys are shown once at creation and stored hashed (SHA-256). You can revoke them anytime
from the dashboard. Three invalid credentials from the same IP within 5 minutes triggers a temporary 429.
The dashboard itself uses a session cookie set after Google sign-in β you don't need an API key for the UI.
Base URL & format
- Base URL:
https://assistant.voicefleet.ai - Request bodies: JSON (
Content-Type: application/json) - Responses: JSON, UTF-8
- Timestamps: ISO 8601 (UTC)
- Phone numbers: E.164 (
+13125550100) β required on bind
Telecom providers
A phone number is bound to one telecom provider via the provider
field on the binding. The provider determines which webhook URL to configure on the
provider side, and how the live media is bridged.
| Provider | Webhook URL | Inbound | Outbound dial | Transfer / DTMF |
|---|---|---|---|---|
| Telnyx | /telnyx/route |
Full bridge (TeXML + media stream) | Yes (TeXML Calls API) | Yes (Call Control) |
| Twilio | /twilio/route |
Full bridge (TwiML <Connect><Stream>) |
Yes (REST /Calls.json) |
Yes (Update Call w/ TwiML) |
| Voximplant | /voximplant/route (JSON) |
Full bridge (VoxEngine scenario relays slin16 PCM) | Yes (Management API StartScenarios) |
No (use Voximplant SDK directly) |
| VoIPCloud | /voipcloud/route (JSON) |
Routing-only β PBX forwards to a sibling number on Telnyx/Twilio | Click-to-call (call-to-number API) |
No (use BYO SIP trunk pattern) |
Telnyx setup
- Create a TeXML application; set Inbound voice URL to
https://assistant.voicefleet.ai/telnyx/route. - Assign your number to that application.
- (Optional) Set
TELNYX_ROUTE_SECRETin your tenant env and add headerx-telnyx-route-secreton the app. - For outbound + transfer + DTMF, also set
TELNYX_API_KEYandTELNYX_CONNECTION_IDserver-side.
Twilio setup
- In the Twilio console, open the phone number β Voice β A call comes in.
- Set webhook to
https://assistant.voicefleet.ai/twilio/route(HTTP POST). - Bind the number with
"provider": "twilio"viaPUT /numbers/:number. - For outbound + transfer + DTMF set
TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENserver-side.
Voximplant setup
Voximplant scenarios run JavaScript in their cloud β bind a minimal scenario like this:
// VoxEngine scenario β fetch our routing decision then bridge raw audio.
require(Modules.WebSocket);
VoxEngine.addEventListener(AppEvents.CallAlerting, async (e) => {
const call = e.call;
call.answer();
const r = await fetch('https://assistant.voicefleet.ai/voximplant/route', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'To=' + encodeURIComponent(call.number()) + '&From=' + encodeURIComponent(call.callerid()),
});
const action = await r.json();
if (action.action !== 'assistantfleet') { call.hangup(); return; }
const ws = VoxEngine.createWebSocket(action.websocket_url);
ws.addEventListener(WebSocketEvents.OPEN, () => {
call.sendMediaTo(ws, { encoding: 'pcm', sampleRate: 16000 });
ws.sendMediaTo(call);
});
ws.addEventListener(WebSocketEvents.CLOSE, () => call.hangup());
});
Audio format must be slin16 (16-bit linear PCM, mono, 16 kHz) β that's what
/media/voximplant expects. The scenario is the customer's responsibility;
we don't run code in Voximplant.
For outbound, configure a second scenario (or extend this one) that reads
VoxEngine.customData(), calls VoxEngine.callPSTN(), and bridges to our WS.
See the template under Outbound calls. Set VOXIMPLANT_RULE_ID
to the Rule that points at that scenario, plus VOXIMPLANT_ACCOUNT_ID and
VOXIMPLANT_API_KEY server-side.
VoIPCloud setup
VoIPCloud is a PBX / SIP-trunk provider operating in five regions:
ukβhttps://uk.voipcloud.onlineieβhttps://ie.voipcloud.onlinenzβhttps://nz.voipcloud.onlineusβhttps://us.voipcloud.onlinecaβhttps://ca.voipcloud.online
Each binding carries its own region, so a single AssistantFleet deployment can
serve VoIPCloud numbers across regions. We don't terminate SIP. There are two paths:
Inbound (PBX β AI):
- Bind your VoIPCloud number with
"provider": "voipcloud", "region": "uk"(or your region). - Also bind a Telnyx or Twilio number to the same assistant β that's where calls actually bridge.
- Configure your VoIPCloud PBX to query
/voipcloud/routefor the forward target; we respond with the sibling Telnyx/Twilio number. - The PBX SIP-forwards the call there; the streaming provider picks it up.
Outbound (click-to-call) β verified against VoIPCloud's /api/integration/v2/call-to-number:
- Put
VOIPCLOUD_API_KEYin the server env. The base URL is derived from each binding'sregion; you can override withVOIPCLOUD_BASE_URLfor single-region installs or testing. - Have a VoIPCloud user/extension whose dial behavior forwards to a sibling Telnyx/Twilio AI number β that puts the AI on the calling leg after the bridge.
- POST
/calls/outboundwithuser_numberset to that VoIPCloud extension. VoIPCloud rings it first, then dialstoand bridges.
The Vapi-style AI is the calling leg directly via SIP pattern (used by their
warm-transfer-experimental + sipVerb: dial) requires a SIP UAC in the AI
stack β Vapi has one, AssistantFleet doesn't. If that's what you need, use Telnyx/Twilio for
outbound or peer their BYO-SIP trunk through your VoIPCloud trunk on the provider side.
Assistants
Create
POST /assistants
| Field | Type | Notes |
|---|---|---|
name | string | Required. |
instructions | string | System prompt for the agent. |
first_message | string | Greeting spoken when the call connects. |
greeting_mode | string | natural (default for new assistants) lets the model vary phrasing and cadence while preserving facts; exact speaks the greeting verbatim. Existing assistants migrate to exact. |
model | string | See models. Default gpt-realtime-2.1. |
voice | string | See voices. Default marin. |
transcription_model | string|null | Caller transcript model. Omit to snapshot the current default; null follows OPENAI_TRANSCRIPTION_MODEL. See input transcription models. |
language | string | ISO 639-1, default en. Also hints the caller transcript model for better accuracy and latency. |
turn_detection | string | One of semantic_vad (default for new assistants), server_vad, none. |
turn_eagerness | string | Semantic VAD timing: auto (default), low, medium, or high. Lower values let callers pause longer. |
reasoning_effort | string | Realtime 2 reasoning: minimal, low (default), medium, high, or xhigh. Higher values can add latency and cost. |
max_output_tokens | integer | Cap per response. Default 600. |
end_call_enabled | boolean | Whether the assistant can hang up via end_call. Default true. See built-ins. |
transfer_phone_numbers | string[] | E.164 list. If non-empty, exposes transfer_call. See built-ins. |
dtmf_enabled | boolean | Exposes send_dtmf so the assistant can press keys (e.g. navigating IVRs on outbound). Default false. See DTMF. |
voicemail_action | string | One of continue (default), leave_message, hang_up. Outbound only. See voicemail. |
voicemail_message | string | Spoken verbatim when voicemail_action: "leave_message". |
Returns 201 with the created assistant (includes id, created_at).
List, get, update, delete
GET /assistants
GET /assistants/:id
PATCH /assistants/:id # any subset of the create fields
DELETE /assistants/:id # 204 on success; unbinds its numbers
Example: update an assistant's prompt
curl -X PATCH https://assistant.voicefleet.ai/assistants/abc123 \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"instructions": "You are a senior support agent. Stay polite under pressure."}'
Phone numbers
A phone number is bound to exactly one assistant at a time, and a number is globally unique β you cannot bind a number that another tenant has already claimed.
Bind
PUT /numbers/:number β body {"assistant_id": "<id>", "provider": "telnyx"}
The path parameter must be E.164. provider defaults to telnyx β see Telecom providers for the supported values. When provider is voipcloud, also pass region: one of uk | ie | nz | us | ca (defaults to uk). The region selects the regional VoIPCloud portal (https://{region}.voipcloud.online) used for the Integration API.
List
GET /numbers
# β [{ "phone_number": "13125550100", "assistant_id": "abc", "provider": "telnyx", "region": null, "created_at": "β¦" }, β¦]
Unbind
DELETE /numbers/+13125550100
# β 204 No Content
Call logs
GET /calls?page=1&limit=25&sort=started_at&order=desc
Returns a tenant-scoped page of calls. Aggregate stats reflect the active filters.
Filters: search, assistant_id, source, model,
voice, outcome, sentiment, date_from, and
date_to. Sort by started_at, source,
duration_seconds, from_number, did,
assistant_name, model, voice, outcome,
ended_reason, sentiment, or cost_total; order is asc or
desc. The maximum page size is 1000.
{
"stats": { "calls": 42, "totalSeconds": 3120, "totalCost": 5.18 },
"calls": [
{
"id": "β¦",
"started_at": "2026-05-24T12:01:33Z",
"ended_at": "2026-05-24T12:03:01Z",
"duration_seconds": 88,
"source": "telnyx", // or "web"
"did": "13125550100",
"from_number": "13644448442",
"assistant_id": "abc",
"assistant_name": "Front desk",
"model": "gpt-realtime-2.1",
"voice": "marin",
"ended_reason": "assistant_ended_call",
"recording_available": true,
"recording_url": "/calls/β¦/recording",
"waveform_url": "/calls/β¦/waveform",
"cost_total": 0.1432,
"cost_openai": 0.1378,
"cost_telnyx": 0.0054,
"transcript": [
{ "role": "assistant", "text": "Thanks for callingβ¦", "t": "2026-05-24T12:01:34Z" },
{ "role": "user", "text": "Hi, I'd like to bookβ¦", "t": "2026-05-24T12:01:38Z" }
],
"tool_events": [
{
"type": "tool",
"call_id": "call_β¦",
"name": "check_availability",
"arguments": { "date": "2026-05-24" },
"output": { "available": true },
"status": "completed",
"started_at": "2026-05-24T12:01:42Z",
"completed_at": "2026-05-24T12:01:42.240Z",
"duration_ms": 240
}
]
}
],
"pagination": {
"total": 42,
"page": 1,
"limit": 25,
"total_pages": 2,
"sort": "started_at",
"order": "desc"
}
}
tool_events are ordered into the dashboard transcript timeline by
started_at. Stored payloads include bounded model arguments and tool responses,
but never configured tool URLs, headers, or signing secrets.
Play a recording
GET /calls/:id/recording
Returns the mixed call audio as audio/wav. The endpoint uses the same tenant
authentication as the rest of the management API and supports browser range requests.
Recordings are available for calls made after recording support is deployed; older rows
cannot be reconstructed from their transcripts.
GET /calls/:id/waveform
Returns compact 0β255 peak arrays used by the dashboard's interactive waveform. New
recordings include separate caller and assistant channels; recordings
created before waveform support are analyzed lazily and returned as a single
mixed channel. This endpoint is authenticated and tenant-scoped too.
Recording is enabled by default. Set ASSISTANTFLEET_RECORDINGS_ENABLED=0 to
disable it or ASSISTANTFLEET_RECORDINGS_DIR to change storage. Make sure your
call flow complies with the recording-consent laws that apply to your callers.
Post-call analytics
Every call gets a cheap LLM pass after it ends β one structured-JSON call to gpt-4o-mini
on the transcript. The result is stored on the call_logs row and visible in the
Call logs view + included in GET /calls responses.
| Field | Type | Notes |
|---|---|---|
summary | string | 1β2 sentences. Neutral, grounded in transcript. |
sentiment | enum | positive | neutral | negative |
outcome | enum | resolved | follow_up_required | booking_request | complaint | voicemail | abandoned | other |
action_items | string[] | Short concrete to-dos for a human teammate (max 20). |
analytics_at | iso8601 | When the analysis ran (null = pending or disabled). |
Fire-and-forget β analytics never blocks call teardown or webhook delivery. If the LLM call
fails or the transcript is empty, the row stays null. Disable with
OPENAI_ANALYTICS_ENABLED=0; tune model with OPENAI_ANALYTICS_MODEL.
Metrics & latency
GET /metrics returns a JSON snapshot of the in-memory metrics registry
(resets on process restart β not Prometheus-persistent). Admin sees aggregate; tenants see
their own slice. Use it to sanity-check capacity and turn-latency on real calls.
curl https://assistant.voicefleet.ai/metrics \
-H "Authorization: Bearer af_live_YOUR_KEY"
{
"uptime_seconds": 12483,
"active_calls": 2,
"calls_started": 145,
"calls_ended": 143,
"calls_by_provider": { "telnyx": 138, "twilio": 7 },
"latencies": {
"setup_ms": { "n": 143, "min": 220, "p50": 410, "p95": 720, "p99": 980, "max": 1200 },
"greeting_ms": { "n": 142, "min": 320, "p50": 560, "p95": 940, "p99": 1300, "max": 1600 },
"turn_ms": { "n": 412, "min": 380, "p50": 720, "p95": 1180, "p99": 1450, "max": 2100 },
"queue_depth_peak": { "n": 143, "min": 3, "p50": 22, "p95": 58, "p99": 91, "max": 110 }
}
}
What each latency means:
setup_msβ WS open βsession.updated. The OpenAI Realtime connection cost.greeting_msβsession.updatedβ first audio delta. Time to "Thanks for callingβ¦".turn_msβ caller speech-start β first response audio delta. What callers actually feel.queue_depth_peakβ max outbound-frame queue size during the call. High values mean we're producing audio faster than the provider can drain it.
Samples are rolling (cap 1000 per metric); old values are dropped. Per-call summary lines also appear in the process logs:
setup=420ms greeting=560ms turns=4 p50=680ms p95=910ms qpeak=22.
Tenant credentials
Each tenant can bring their own provider accounts β Telnyx, Twilio, Voximplant, VoIPCloud β
and they're stored encrypted at rest (AES-256-GCM with the server's
ASSISTANTFLEET_ENCRYPTION_KEY). When a dialer fires it resolves credentials
tenant-first, falling back to the server env if the tenant hasn't configured that provider.
Save credentials
curl -X PUT https://assistant.voicefleet.ai/credentials/twilio \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"account_sid": "AC...", "auth_token": "..."}'
Provider-specific shapes:
| Provider | Required | Optional |
|---|---|---|
telnyx | api_key | connection_id, route_secret, stream_secret |
twilio | account_sid, auth_token | route_secret |
voximplant | account_id, api_key | rule_id, application_id, api_base_url |
voipcloud | api_key | base_url |
List configured providers
GET /credentials
# β [{ "provider": "twilio", "configured": true, "created_at": "β¦", "updated_at": "β¦" }, β¦]
Secret values are never returned by GET β only the fact that a provider is configured and when it was last updated.
Clear credentials
DELETE /credentials/twilio
# β 204
After clearing, the server-env defaults (if any) are used again.
Outbound calls
Dial out from one of your bound Telnyx numbers and bridge the call straight to an assistant β for appointment reminders, follow-ups, surveys, etc.
Start a call
curl -X POST https://assistant.voicefleet.ai/calls/outbound \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "<assistant-id>",
"from": "+13644448442",
"to": "+13125550100"
}'
Returns 201 with Telnyx call ids:
{
"status": "initiated",
"call_sid": "β¦",
"call_control_id": "v3:β¦",
"from": "+13644448442",
"to": "+13125550100",
"assistant_id": "β¦"
}
Rules
frommust be E.164 and bound to your tenant (use PUT /numbers/:number). The dial provider is whatever that number is bound on.tomust be E.164.- Telnyx-from: requires
TELNYX_API_KEY+TELNYX_CONNECTION_IDserver-side, else503. AI is the calling leg. - Twilio-from: requires
TWILIO_ACCOUNT_SID+TWILIO_AUTH_TOKEN, else503. AI is the calling leg. - Voximplant-from: requires
VOXIMPLANT_ACCOUNT_ID+VOXIMPLANT_API_KEY+VOXIMPLANT_RULE_ID(the rule must point at a scenario that readsVoxEngine.customData()and dials viacallPSTNβ template below). AI is the calling leg. - VoIPCloud-from: requires
VOIPCLOUD_API_KEY+VOIPCLOUD_BASE_URL, plus auser_numberfield in your POST body (your VoIPCloud user/extension that will be rung first). This is a click-to-call flow β VoIPCloud rings your user, then bridges toto. Have your user extension forward to a sibling Telnyx/Twilio number bound to the assistant so the AI ends up on that leg. The AI-as-calling-leg path requires SIP origination through VoIPCloud's BYO SIP trunk β we don't ship a SIP UAC; configure your AI provider's BYO SIP credential for that pattern.
Voximplant outbound scenario template
Configure a VoxEngine scenario tied to the rule you set in VOXIMPLANT_RULE_ID:
require(Modules.WebSocket);
VoxEngine.addEventListener(AppEvents.Started, async () => {
const data = JSON.parse(VoxEngine.customData() || '{}');
if (!data.to || !data.websocket_url) { VoxEngine.terminate(); return; }
const call = VoxEngine.callPSTN(data.to, data.from);
call.addEventListener(CallEvents.Connected, () => {
const ws = VoxEngine.createWebSocket(data.websocket_url);
ws.addEventListener(WebSocketEvents.OPEN, () => {
call.sendMediaTo(ws, { encoding: 'pcm', sampleRate: 16000 });
ws.sendMediaTo(call);
});
ws.addEventListener(WebSocketEvents.CLOSE, () => call.hangup());
});
call.addEventListener(CallEvents.Disconnected, () => VoxEngine.terminate());
call.addEventListener(CallEvents.Failed, () => VoxEngine.terminate());
});
VoIPCloud outbound example
curl -X POST https://assistant.voicefleet.ai/calls/outbound \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "<assistant-id>",
"from": "+441216306195", // VoIPCloud DID (caller_id)
"to": "+447418328008", // destination
"user_number": "+44XXXXXXXXXX" // your VoIPCloud user/extension to ring first
}'
Outbound calls fire the same call.started / call.ended webhooks as inbound,
and the resulting call_logs row has direction: "outbound" so you can filter.
Tools
Tools let the assistant call your HTTP endpoint mid-conversation (Vapi-style function calling). You define a tool once, attach it to one or more assistants, and we forward it to OpenAI Realtime as a function. When the model decides to call it, we POST signed JSON to your URL and feed the response back so the model can speak the result.
Create a tool
curl -X POST https://assistant.voicefleet.ai/tools \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "lookup_customer",
"description": "Look up a customer by phone number.",
"url": "https://your-app.com/tools/lookup",
"method": "POST",
"parameters": {
"type": "object",
"properties": { "phone": { "type": "string", "description": "E.164 phone" } },
"required": ["phone"]
}
}'
The response includes a secret shown once β store it; you'll use it to verify our request signature.
Attach to an assistant
curl -X PUT https://assistant.voicefleet.ai/assistants/<assistant-id>/tools \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"tool_ids": ["<tool-id-1>", "<tool-id-2>"]}'
This replaces the assistant's tool set β pass an empty array to detach all.
What your endpoint receives
POST https://your-app.com/tools/lookup
Content-Type: application/json
X-AssistantFleet-Tool: lookup_customer
X-AssistantFleet-Call: <openai call_id>
X-AssistantFleet-Signature: sha256=<hmac of raw body>
{
"call_id": "call_β¦",
"name": "lookup_customer",
"arguments": { "phone": "+13125550100" },
"context": { "assistant_id": "β¦", "did": "13644448442", "from": "15551112222", "source": "telnyx" },
"created_at": "2026-05-24T13:42:01Z"
}
What you return
Respond with JSON β anything serializable. The whole body is fed back to the model as the function output, which the model uses to compose its next spoken turn.
{
"found": true,
"customer": { "name": "Mariano", "tier": "gold", "last_visit": "2026-04-12" }
}
Timeouts (default 8s, configurable per tool 0.5β30s), 5xx, and network errors are returned to
the model as a structured {"error": "..."} envelope so it can recover gracefully
("I'm having trouble looking that up β could you try again in a moment?"). Keep responses short:
long payloads cost more tokens and make the model verbose.
Verifying signatures (Node.js)
app.post('/tools/lookup', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.header('X-AssistantFleet-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.AF_TOOL_SECRET)
.update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(401);
const { arguments: args, context } = JSON.parse(req.body.toString());
// ... your lookup logic ...
res.json({ found: true, customer: { name: 'Mariano' } });
});
Manage tools
GET /tools
GET /tools/:id
PATCH /tools/:id # any subset of the create fields
DELETE /tools/:id # cascades the assistant attachment
Tips
- The model uses the
name+description+ parameter schema to decide when to call the tool. Be explicit and concrete in the description. - Function names follow OpenAI's rule:
^[A-Za-z_][A-Za-z0-9_]{0,63}$. - GET tools receive the arguments as query string instead of a body.
- Tool URLs must be HTTPS (same SSRF guard as webhooks).
Built-in tools
Runtime tools are baked in β you don't define them as custom HTTP tools. Some are always available and others are enabled by assistant settings.
wait_for_user
Always available. When VAD produces a turn containing only silence, background noise, or no intelligible request, the assistant invokes this no-op and waits without speaking. This avoids awkward filler responses to false turns.
end_call
Lets the assistant hang up cleanly when the conversation is done. Enabled by default on every new assistant; toggle off via:
curl -X PATCH https://assistant.voicefleet.ai/assistants/<id> \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"end_call_enabled": false}'
transfer_call
Lets the assistant transfer the live call to a human at one of a whitelist of phone numbers (Telnyx Call Control transfer under the hood). Only exposed when you set the list:
curl -X PATCH https://assistant.voicefleet.ai/assistants/<id> \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"transfer_phone_numbers": ["+13125550100", "+13125550199"]}'
The destinations are echoed to the model in the tool description, so it knows the options. The model is instructed to announce the transfer before invoking the tool.
DTMF (keypad)
Touch-tone digits work in both directions on real phone calls (not in web tests).
Caller β assistant
When a caller presses a key, Telnyx forwards a dtmf media event.
We surface it to the model as a user text turn ("[Caller pressed keypad: 1]")
and trigger a response, so prompts like "Press 1 for sales, 2 for support" just work
if the model is instructed to treat keypad input as a choice.
Assistant β call (e.g. navigating an outbound IVR)
Enable on the assistant β exposes a send_dtmf built-in tool:
curl -X PATCH https://assistant.voicefleet.ai/assistants/<id> \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"dtmf_enabled": true}'
The model can then call send_dtmf({digits: "1234#"}); we relay through Telnyx Call Control send_dtmf. Digits accepted: 0-9 * # w W (w/W = half-second pause).
Voicemail handling (outbound)
For outbound calls only, the assistant can be instructed how to react when it lands on a voicemail / answering machine instead of a human. Three modes:
continue(default) β talk to the machine the same as a human.hang_upβ stay silent and end the call as soon as voicemail cues are detected.leave_messageβ wait for the beep, sayvoicemail_messageverbatim, then hang up.
curl -X PATCH https://assistant.voicefleet.ai/assistants/<id> \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"voicemail_action": "leave_message",
"voicemail_message": "Hi, this is an automated reminder about your appointment tomorrow at 3 PM. Please call us back."
}'
Detection is currently model-based (the model identifies voicemail cues from the greeting audio and a prompt block injected only on outbound calls). It's effective for most mainstream voicemail greetings, but not as deterministic as carrier AMD. If you need stricter handling at scale, we can wire Telnyx AMD later.
Knowledge base
Plain-text documents (FAQs, business info, policies) that get injected into the assistant's system prompt at the start of every call. No embeddings β the documents are sent verbatim, so the model has everything in context. Best for small, stable corpora (hours, prices, scripts).
Limits
- 50 KB per document.
- 200 KB total injected per assistant per call (excess is truncated).
- Token cost: the docs ride the cacheable prefix of the prompt, so repeated calls hit the cache (~98% discount on cached input tokens).
Create
curl -X POST https://assistant.voicefleet.ai/knowledge \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "business-hours",
"content": "We are open Monday through Friday 9 AM to 6 PM Central, Saturday 10β4. Closed Sundays and federal holidays."
}'
Attach to an assistant
curl -X PUT https://assistant.voicefleet.ai/assistants/<id>/knowledge \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"knowledge_ids": ["<doc-id-1>", "<doc-id-2>"]}'
This replaces the attached set β pass an empty array to detach all.
Manage
GET /knowledge
GET /knowledge/:id
PATCH /knowledge/:id # any subset of name/content
DELETE /knowledge/:id
Webhooks
We POST signed JSON to your URL when calls start and end. Create a webhook in the dashboard or via API:
curl -X POST https://assistant.voicefleet.ai/webhooks \
-H "Authorization: Bearer af_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.com/hooks/assistantfleet", "events": ["call.started", "call.ended"]}'
The response includes the signing secret β shown once.
Event payload
POST https://your-app.com/hooks/assistantfleet
Content-Type: application/json
X-AssistantFleet-Event: call.ended
X-AssistantFleet-Delivery: 9f8a-β¦
X-AssistantFleet-Signature: sha256=<hex hmac of raw body>
{
"event": "call.ended",
"delivery": "9f8a-β¦",
"created_at": "2026-05-24T12:03:02Z",
"data": { /* same shape as the GET /calls row */ }
}
Verifying signatures (Node.js)
const crypto = require('crypto');
app.post('/hooks/assistantfleet', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.header('X-AssistantFleet-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.AF_WEBHOOK_SECRET)
.update(req.body) // raw Buffer
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// handle event.event === 'call.started' | 'call.ended'
res.status(204).end();
});
Retries
We retry 5xx and 429 with exponential backoff (default up to 4 retries).
Other 4xx responses are not retried. Replies are expected within 5 seconds β return early and
process asynchronously.
Manage webhooks
GET /webhooks
PATCH /webhooks/:id # body { enabled: false } to pause
DELETE /webhooks/:id
Models & voices
Models
AssistantFleet offers the current general-purpose, bidirectional OpenAI Realtime models. Translation-only and transcription-only models are not assistant replacements and are not shown.
gpt-realtime-2.1β latest flagship reasoning voice model with tool use; current AssistantFleet default.gpt-realtime-2.1-miniβ latest faster, lower-cost reasoning voice model.gpt-realtime-2β previous flagship generation.gpt-realtime-1.5β flagship audio model for voice agents and customer support.gpt-realtimegpt-realtime-miniβ cheapest, good for simple flows.
Existing assistants keep any previously stored model identifier, including older models that are no longer offered for new selections. Availability still depends on the models enabled for your OpenAI account.
Input transcription models
This setting controls the separate asynchronous caller transcript used in the dashboard and call logs. The Realtime assistant consumes the original audio directly, so its understanding can differ from the displayed transcript.
gpt-4o-transcribeβ best accuracy; recommended when transcript quality matters most.gpt-4o-mini-transcribeβ lower-cost default.whisper-1β legacy compatibility.
Voices
alloy, ash, ballad, cedar, coral,
echo, marin, sage, shimmer, verse.
OpenAI recommends marin and cedar for best quality.
Errors
All errors come back as JSON with the same shape:
{ "error": { "message": "name is required" } }
| Status | Meaning |
|---|---|
| 400 | Invalid input (missing field, bad enum value, etc.) |
| 401 | Missing or invalid API key / session. |
| 404 | Resource doesn't exist for your tenant. |
| 429 | IP temporarily blocked after repeated invalid credentials. |
| 500 | Server error. |