SautiKit
/ai-agents/pricing/docs/api/blog
sign inStart building

AI support voice agent: answer, look up, resolve — escalate with a summary

Put a managed AI agent on your support line. It looks up orders through your signed API tools, resolves routine calls, and escalates with a summary.

use-caseai-voice-agentai-agentscustomer-supportinbound

Next Steps

  • Route Inbound Calls to an AI AgentBind a published AI agent to answer inbound calls on a number (always or after-hours), respond to the CallHandover callback when the agent hands back to a human, and chain specialist agents.
  • AI receptionist: deploy an AI agent on your number in minutesCreate an AI agent, publish it, and bind it to your number in three API calls. It answers callers in real time and hands over to your team with a summary. The BYO Stream recipe is the second half.
  • Voice broadcast campaigns: outbound AI calls at scaleCreate a broadcast on a published AI agent with per-contact templates, load contacts, and start. The dialer keeps to calling windows, retries no-answers and voicemail, and reports every outcome.
  • Your Sautikit number can now answer itself: managed AI agents are liveLaunch: bind a published AI agent to your Sautikit number and it answers callers in real time — interruptible, tool-calling, with a handover that briefs your team. The same agents dial broadcasts.
SautiKit

Programmable voice infrastructure for Africa. Buy numbers, place calls, and bill per second, all in local currency, via API.

All systems operational

Product

AI voice agentsBroadcastsNumbersCalls & routingRecordingsWallet & billingPricing

Developers

DocumentationAPI referenceVoice actionsWebhooksErrorsMCP serverQuickstartAI prompt

Compare

vs Africa's Talkingvs Twiliovs Infobipvs Vapi, Retell & BlandMigrate from Africa's TalkingAll comparisons

Company

AboutBlogConsole

© 2026 Sautikit. All rights reserved • Powered by Helloduty

Terms of ServicePrivacy Policy

Sautikit provides voice API services for application developers. Numbers provisioned on this platform are not configured for emergency calling (e.g. 999 / 112). Do not use Sautikit numbers as a replacement for a primary phone line.

Summary

An AI customer-support voice agent answers inbound support calls, resolves the routine ones, and escalates the rest. It handles FAQs, looks up account or order context from your backend mid-call, walks the caller through simple fixes, and connects a human agent on the same call when it hits something it cannot solve. The goal is deflection: most callers get an answer without ever waiting in a queue.

Sautikit's managed AI agents run this end to end. You write the agent's brief as a prompt, attach HTTP tools that call your API — each request HMAC-signed so your endpoint can verify it came from your agent — publish, and bind the agent to your support number. When the agent escalates, your team does not get a cold transfer: the handover request carries the agent's reason and a summary of everything the caller already said.

Want your own model in the loop instead? The build-it-yourself Stream recipe is in the second half of this page.

Who this is for

  • Fintechs, ISPs, and e-commerce teams running a support line where most calls are balance checks, order status, outage status, or plan questions.
  • Backend teams that already have account, billing, or ticketing APIs and want a voice front end over them — without building a voice stack.
  • Product managers who need to cut queue times and human-agent minutes without dropping the option to reach a person.
  • Teams that want the deflection live this week, with the option to swap in their own model later.

The fast path: a managed agent with your tools

1. Create the agent

The base_prompt sets the agent's job and its boundaries. Models come from the Sautikit catalog (GET /v1/models, ids like sautikit-flash).

const res = await fetch("https://api.sautikit.com/v1/agents", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Support line",
    base_prompt:
      "You are the support agent for Wema Stores. Help callers with order status, delivery windows, and returns. Use the order lookup tool before answering any order question. If the caller disputes a charge or asks for a human, hand over.",
    model: "sautikit-flash",
    voice: "Woman",
    max_call_minutes: 10,
  }),
});
 
const agent = await res.json();
// agent.tool_signing_secret is returned once, here — store it.

The create response includes the agent's tool_signing_secret exactly once. Your API uses it to verify tool-call signatures below.

2. Attach a tool that hits your API

Tools are plain HTTP endpoints on your side. You describe the parameters with JSON Schema; the model decides when to call the tool and fills the arguments from the conversation. Every request is HMAC-signed with the agent's signing secret, so your endpoint can reject anything that did not come from your agent.

const toolId = crypto.randomUUID();
 
await fetch(`https://api.sautikit.com/v1/agents/${agent.id}/tools/${toolId}`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "lookup_order",
    description:
      "Fetch the status, items, and delivery window for a customer's order.",
    url: "https://api.wemastores.example.com/agent/orders/lookup",
    method: "POST",
    parameters: {
      type: "object",
      properties: {
        order_number: { type: "string", description: "e.g. WS-4821" },
        phone: { type: "string", description: "Caller's number in E.164" },
      },
      required: ["order_number"],
    },
    timeout_ms: 5000,
  }),
});

Whatever JSON your endpoint returns goes back into the conversation — the agent reads it and answers the caller in plain language. The same mechanism handles booking changes, ticket creation, or CRM writes: any endpoint you are willing to expose to your own agent.

3. Publish and bind to your support number

await fetch(`https://api.sautikit.com/v1/agents/${agent.id}/publish`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}` },
});
 
await fetch(`https://api.sautikit.com/v1/numbers/${NUMBER_ID}/routing`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    inbound_agent: {
      enabled: true,
      agent_id: agent.id,
      mode: "always",
      handover_callback_url: "https://api.wemastores.example.com/voice",
    },
  }),
});

mode: "always" puts the agent on every call; mode: "after_hours" with a schedule keeps humans on the line during business hours and the agent covering nights and weekends — the routing guide shows both. If no AI seat is free or the agent is unpublished, calls fall through to your normal routing; callers never hear an error.

On the call

The conversation is real-time and interruptible — a caller who cuts in mid-sentence is heard, and the agent adjusts. Tool calls happen inside the conversation, so "where is my order WS-4821" becomes a lookup against your API and a spoken answer in the same breath. The agent can transfer the caller to a specialist agent in the same session (billing questions to a billing agent, for example), end the call itself when the caller is done, and is hard-capped by max_call_minutes (default 15).

Escalation with a summary, not a cold transfer

When the agent hands over — the caller asked for a person, or the issue went beyond its brief — Sautikit POSTs a CallHandover request to your handover_callback_url with the reason and a summary:

{
  "sessionId": "HD_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "callerNumber": "+254700000001",
  "status": "CallHandover",
  "handover": {
    "agent_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",
    "agent_name": "Support line",
    "reason": "caller disputes a delivery charge",
    "summary": "Caller wants a refund for order WS-4821; order exists and shows delivered Tuesday; caller says it never arrived."
  }
}

You reply with normal JSON voice actions — a dial to your support desk — and the human joins a call where identity is confirmed, the order is found, and the complaint is already written down:

{
  "actions": [
    { "say": { "text": "Let me get a specialist on the line." } },
    { "dial": { "number": "+254720000010" } }
  ]
}

QA and pricing

Calls are recorded by default, and GET /v1/calls/{id}?transcript=1 returns the per-turn transcript — roles, timestamps, interruptions, and every tool call with its arguments — so you can audit exactly what the agent looked up and said before refining the prompt.

AI time bills at KES 4.00 per minute, metered per second, on top of the standard per-second call rates; a deflected 50-second FAQ costs 50 seconds, not a rounded-up minute. The free tier includes 1 concurrent AI call (paid tiers: 5, 8, or 10), a dedicated number is KES 116 per month claimed instantly, and the wallet is prepaid via M-Pesa. Every call the agent resolves end to end is a human-agent minute you did not pay a person for.

ℹ

The hosted Sautikit MCP server exposes this whole flow — create_agent, upsert_agent_tool, publish_agent, update_number_routing — so an AI assistant like Claude can stand up your support agent from a conversation.

Prefer to run your own model?

If you want your own LLM driving the conversation — your prompts, your tools, your inference bill — Sautikit forks the live call audio to your server and you own everything above the media layer.

Your number's routing_url returns a <Stream> voice action; Sautikit forks live caller audio to your WebSocket server; your bridge relays that audio to the LLM of your choice and streams synthesized speech back into the call. Because your server owns the conversation, the same LLM turn that decides "I can't resolve this" can end the stream and return a <Dial> to a human.

How it works

  1. A caller dials your Sautikit number. Its routing_url points at your voice webhook.
  2. Your webhook returns an XML <Response> containing a <Stream> action with your wss:// URL.
  3. Sautikit opens a WebSocket to your server. Your server must advertise the audio.drachtio.org subprotocol on the handshake, or Sautikit rejects the connection.
  4. Sautikit forks live caller audio to that socket as binary frames: 16-bit little-endian PCM at the outputSamplingRate you requested (use 16000 for AI models).
  5. Your bridge relays the audio to your LLM. The model can call your internal tools and APIs mid-conversation to pull account context, order status, or ticket history.
  6. You send synthesized PCM back on the same socket. Sautikit plays it into the call. This continues turn by turn.
  7. When the agent can't resolve the issue, your flow stops the stream and returns a <Dial> to a human agent, connecting them on the same call.
ℹ

Real-time streaming ships via the raw XML <Stream> element. Native JSON stream support is on the roadmap; until then, return <Stream> in an application/xml response. See the voice actions concept.

Your server owns the conversation state. Key the LLM session and any tool results by the call SID from the stream handshake, so a mid-call escalation to <Dial> can pass everything the human agent needs (verified identity, account ID, what the caller already tried).

<Stream> attributes

AttributeRequiredNotes
urlyeswss:// endpoint Sautikit connects to.
trackyesinbound_track, outbound_track, or both_tracks.
connectyesconnect="true" answers and holds the call leg for the fork's lifetime. Without it the document ends immediately and the call hangs up.
outputSamplingRateyes8000 or 16000. Use 16000 for AI models. This is the rate delivered to your socket.
bidirectionalSamplingRatenoRate your server sends PCM back at; Sautikit resamples it to the channel codec. Without it, bidirectional playback behavior is undefined.
namenoFriendly identifier echoed in stream status events.
headerMetadatanoFlat JSON object of string key/value pairs (auth tokens, tenant/correlation IDs), folded into the first WebSocket text frame alongside openMetadata — not HTTP handshake headers.
openMetadatanoOpaque UTF-8 payload sent as the first text frame.
statusCallbacknoURL Sautikit POSTs stream status events to.
statusEventsnoSpace-separated subset of stream-started, stream-stopped, stream-error.

Audio on the socket is 16-bit little-endian PCM. Your server must accept the audio.drachtio.org subprotocol.

1. The XML your webhook returns

When the number is dialled, Sautikit POSTs to your routing_url. Reply with application/xml:

<Response>
  <Stream
    name="support-agent"
    url="wss://your-app.example.com/audio"
    track="both_tracks"
    connect="true"
    outputSamplingRate="16000"
    bidirectionalSamplingRate="16000"
    statusCallback="https://your-app.example.com/stream-status"
    statusEvents="stream-started stream-stopped stream-error" />
</Response>

2. WebSocket bridge (Node.js)

This sketch shows where the LLM plugs in, where you'd invoke an internal lookup, and how you'd signal an escalation. Wire the LLM client and PCM plumbing to your provider.

import { WebSocketServer } from "ws";
 
// Sautikit negotiates the `audio.drachtio.org` subprotocol on connect.
const wss = new WebSocketServer({
  port: 8080,
  handleProtocols: (protocols) =>
    protocols.has("audio.drachtio.org") ? "audio.drachtio.org" : false,
});
 
wss.on("connection", (ws) => {
  const llm = startLLMSession({
    // Your internal tools the model can call mid-conversation.
    tools: {
      async getAccount({ msisdn }) {
        const res = await fetch(
          `https://internal.example.com/accounts?phone=${msisdn}`,
        );
        return res.json(); // balance, plan, open tickets, outage status...
      },
    },
    // The model calls this when it can't resolve the issue.
    onEscalate: (reason) => escalateToHuman(ws, reason),
  });
 
  ws.on("message", (data, isBinary) => {
    if (isBinary) {
      // Live caller audio: 16-bit LE PCM at 16000 Hz. Feed it to the model.
      llm.pushAudio(data);
    }
  });
 
  // Model output: PCM back on the same socket. Sautikit plays it into the call.
  llm.on("audio", (pcm) => ws.send(pcm, { binary: true }));
});
 
function escalateToHuman(ws, reason) {
  // Close the stream, then return a <Dial> from your routing flow so the
  // caller is connected to a human agent on the same call.
  ws.close();
  // e.g. redirect the call to a webhook that responds with:
  //   <Response><Dial><Number>+254720000010</Number></Dial></Response>
}
✎

<Stream> forks audio; it does not, by itself, end or redirect the call. To escalate, hand the call back to your routing flow and return a <Dial> to your human agent (or an SIP address / queue) as the next action.

BYO pricing notes

  • Call time: the inbound leg is billed per second for as long as the call is live on Sautikit, including the AI-handled portion. Per-second billing means a 40-second deflected FAQ costs 40 seconds, not a rounded-up minute.
  • Escalation leg: once <Dial> connects a human agent, the per-second rate continues across the connected legs.
  • LLM cost: on this path, model inference runs on your provider and is billed by them, not by Sautikit — there is no Sautikit AI-minute charge.

Next steps

  • Route inbound calls to an AI agent: binding, after-hours schedules, and the full CallHandover contract.
  • AI agents: the managed product page — capabilities, tiers, and pricing.
  • AI receptionist use case: the front-desk variant of the same managed pattern.
  • Voice broadcast campaigns: the same agents dialling outbound at scale.
  • Voice actions concept: full <Stream> reference for the build-your-own path.