πŸ›« AssistantFleet

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

  1. Sign in at assistant.voicefleet.ai with Google. Your tenant is provisioned on first login.
  2. Create an API key in the dashboard under API keys. Copy it once β€” it starts with af_live_.
  3. 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"
      }'
  4. 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"}'

    provider defaults to telnyx. See Telecom providers for the per-provider webhook URL and setup.

  5. 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)
    Call the number β€” you should reach the assistant.

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.

ProviderWebhook URLInboundOutbound dialTransfer / 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

  1. Create a TeXML application; set Inbound voice URL to https://assistant.voicefleet.ai/telnyx/route.
  2. Assign your number to that application.
  3. (Optional) Set TELNYX_ROUTE_SECRET in your tenant env and add header x-telnyx-route-secret on the app.
  4. For outbound + transfer + DTMF, also set TELNYX_API_KEY and TELNYX_CONNECTION_ID server-side.

Twilio setup

  1. In the Twilio console, open the phone number β†’ Voice β†’ A call comes in.
  2. Set webhook to https://assistant.voicefleet.ai/twilio/route (HTTP POST).
  3. Bind the number with "provider": "twilio" via PUT /numbers/:number.
  4. For outbound + transfer + DTMF set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN server-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.online
  • ie β†’ https://ie.voipcloud.online
  • nz β†’ https://nz.voipcloud.online
  • us β†’ https://us.voipcloud.online
  • ca β†’ 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):

  1. Bind your VoIPCloud number with "provider": "voipcloud", "region": "uk" (or your region).
  2. Also bind a Telnyx or Twilio number to the same assistant β€” that's where calls actually bridge.
  3. Configure your VoIPCloud PBX to query /voipcloud/route for the forward target; we respond with the sibling Telnyx/Twilio number.
  4. 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:

  1. Put VOIPCLOUD_API_KEY in the server env. The base URL is derived from each binding's region; you can override with VOIPCLOUD_BASE_URL for single-region installs or testing.
  2. 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.
  3. POST /calls/outbound with user_number set to that VoIPCloud extension. VoIPCloud rings it first, then dials to and 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

FieldTypeNotes
namestringRequired.
instructionsstringSystem prompt for the agent.
first_messagestringGreeting spoken when the call connects.
greeting_modestringnatural (default for new assistants) lets the model vary phrasing and cadence while preserving facts; exact speaks the greeting verbatim. Existing assistants migrate to exact.
modelstringSee models. Default gpt-realtime-2.1.
voicestringSee voices. Default marin.
transcription_modelstring|nullCaller transcript model. Omit to snapshot the current default; null follows OPENAI_TRANSCRIPTION_MODEL. See input transcription models.
languagestringISO 639-1, default en. Also hints the caller transcript model for better accuracy and latency.
turn_detectionstringOne of semantic_vad (default for new assistants), server_vad, none.
turn_eagernessstringSemantic VAD timing: auto (default), low, medium, or high. Lower values let callers pause longer.
reasoning_effortstringRealtime 2 reasoning: minimal, low (default), medium, high, or xhigh. Higher values can add latency and cost.
max_output_tokensintegerCap per response. Default 600.
end_call_enabledbooleanWhether the assistant can hang up via end_call. Default true. See built-ins.
transfer_phone_numbersstring[]E.164 list. If non-empty, exposes transfer_call. See built-ins.
dtmf_enabledbooleanExposes send_dtmf so the assistant can press keys (e.g. navigating IVRs on outbound). Default false. See DTMF.
voicemail_actionstringOne of continue (default), leave_message, hang_up. Outbound only. See voicemail.
voicemail_messagestringSpoken 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.

FieldTypeNotes
summarystring1–2 sentences. Neutral, grounded in transcript.
sentimentenumpositive | neutral | negative
outcomeenumresolved | follow_up_required | booking_request | complaint | voicemail | abandoned | other
action_itemsstring[]Short concrete to-dos for a human teammate (max 20).
analytics_atiso8601When 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:

ProviderRequiredOptional
telnyxapi_keyconnection_id, route_secret, stream_secret
twilioaccount_sid, auth_tokenroute_secret
voximplantaccount_id, api_keyrule_id, application_id, api_base_url
voipcloudapi_keybase_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

  • from must be E.164 and bound to your tenant (use PUT /numbers/:number). The dial provider is whatever that number is bound on.
  • to must be E.164.
  • Telnyx-from: requires TELNYX_API_KEY + TELNYX_CONNECTION_ID server-side, else 503. AI is the calling leg.
  • Twilio-from: requires TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN, else 503. 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 reads VoxEngine.customData() and dials via callPSTN β€” template below). AI is the calling leg.
  • VoIPCloud-from: requires VOIPCLOUD_API_KEY + VOIPCLOUD_BASE_URL, plus a user_number field 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 to to. 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, say voicemail_message verbatim, 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-realtime
  • gpt-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" } }
StatusMeaning
400Invalid input (missing field, bad enum value, etc.)
401Missing or invalid API key / session.
404Resource doesn't exist for your tenant.
429IP temporarily blocked after repeated invalid credentials.
500Server error.