wixzel_voice 0.10.0
wixzel_voice: ^0.10.0 copied to clipboard
Official Dart SDK for the Wixzel Voice API. Build AI agents that place and answer real phone calls over your own SIP trunk, from Dart and Flutter.
wixzel_voice #
Wixzel Voice is an API for AI voice agents. Your agents answer and place real phone calls on your own carrier, and talk to people inside your apps and websites.
This is its official Dart SDK. It works in Dart and Flutter, on servers, phones and the web.

Install #
dart pub add wixzel_voice
Get an API key from the dashboard at voice.wixzel.com. Keys start with wv_live_ or wv_test_.
Quickstart #
Create an agent, then have it call a phone:
import 'package:wixzel_voice/wixzel_voice.dart';
Future<void> main() async {
final client = WixzelVoice(apiKey: 'wv_live_...');
final agent = await client.agents.create(CreateAgent(
name: 'Front desk',
systemPrompt: 'You answer calls for a dental clinic. Keep replies short.',
openingMessage: 'Hello, thanks for calling. How can I help?',
voice: VoiceConfig.realtimeModel(
const RealtimeConfig(model: 'google/gemini-live', voice: 'Charon'),
),
));
// This rings a real phone and spends credit.
final call = await client.calls.create(
CreateCall(to: '+919876543210', agentId: agent.id),
);
print('Calling: ${call.id}');
client.close();
}
A call needs a carrier connected first. See Phone setup. To talk to an agent without a phone number, see Talk without a phone number.
How it works #

- An agent is a prompt plus a voice engine. Every agent also has a mascot, an animated face.
- You connect your own carrier (Twilio, Telnyx, Plivo, Vonage, Exotel, Vobiz or any SIP trunk). Wixzel Voice does not resell minutes or numbers. Your carrier bills you at its own price.
- A phone number on your carrier is pointed at an agent. Calls to it are answered by the agent. Calls from it are placed with
calls.create. - Without a phone number, an agent talks in your app or website over a realtime session.
- You pay for what you use from a prepaid credit balance. Amounts are in micro-USD: 1,000,000 is $1.00.
Contents #
- Setup: the client, API keys, errors, retries, pagination
- Phone setup: carriers, SIP trunks and phone numbers
- Agents: create one, choose the voice, generate one from a website
- Mascots: the default look, customize it, share it, draw it in Flutter
- Calls: place calls, read transcripts, talk without a phone number
- Campaigns and leads, Appointments, Callers
- Knowledge bases, Tools and MCP integrations, Cloned voices, Webhooks
- Usage and balance, Engines and pricing
- Using it with AI coding agents
- Every resource and its methods
The snippets below import package:wixzel_voice/wixzel_voice.dart. Those that read files use dart:io, and those with audio use Int16List from dart:typed_data.
Setup #
The client and API keys #
import 'dart:io';
import 'package:wixzel_voice/wixzel_voice.dart';
Future<void> setup() async {
final client = WixzelVoice(apiKey: Platform.environment['WIXZEL_API_KEY']!);
print(client.keyMode); // KeyMode.live or KeyMode.test
// A key that can only read calls, for a reporting job.
final key = await client.apiKeys.create(const CreateApiKey(
name: 'Reports',
scopes: [ApiScope.callsRead],
));
print(key.key); // shown once; store it now
client.close();
}
Keep keys on your server. In a Flutter app, call your own backend and let it hold the key.
| Option | Default | |
|---|---|---|
apiKey |
required | wv_live_... or wv_test_... |
baseUrl |
https://api.voice.wixzel.com |
For a self-hosted API |
apiVersion |
none | Sends Wixzel-Version, e.g. '2026-09-01' |
timeout |
30 s | Per attempt |
maxRetries |
2 | Retries after the first attempt |
httpClient |
one of its own | Pass your own; then closing it is your job |
defaultHeaders |
none | Sent on every request |
client.request(method, path) reaches any endpoint the SDK does not model yet, with the same auth, retries and errors. client.close() releases the HTTP client when the SDK created it.
Errors #
Every API error is a WixzelException. Match on code. The message is for people and may change.
Future<void> errors(WixzelVoice client, String to, String agentId) async {
try {
await client.calls.create(CreateCall(to: to, agentId: agentId));
} on WixzelException catch (e) {
if (e.code == 'insufficient_credits') print('Balance: ${e.balance}');
print('${e.statusCode} ${e.code} ${e.requestId} ${e.docUrl}');
} on WixzelConnectionException catch (e) {
// No answer, even after retries. Retrying with the same idempotency key is safe.
print(e.message);
}
}
Enums in responses have an unknown value, so a value the API adds later does not break an app you shipped today.
Retries and idempotency #
429: retried afterRetry-After(at most 10 seconds), up tomaxRetries. Rate-limited requests are never charged.- Network failures, timeouts and
502to504: retried only forGETand for requests with an idempotency key. DELETE,PATCHand otherPOSTs are never repeated after an unclear failure.
The paths that spend money (calls.create, agents.testCall, phoneNumbers.purchase, billing.createTopup) send an Idempotency-Key. The SDK makes one per call and reuses it across its own retries. Pass your own to make your own retries safe too:
Future<void> idempotency(WixzelVoice client, String to, String agentId, String orderId) async {
final result = await client.calls.createWithResponse(
CreateCall(to: to, agentId: agentId),
idempotencyKey: 'order-$orderId',
);
if (result.idempotentReplay) print('This call was already placed.');
print(result.data.id);
}
Pagination #
Every list returns a Page. Call autoPaging() to walk every record, or nextPage() to go one page at a time.
Future<void> pages(WixzelVoice client) async {
final calls = await client.calls.list(status: CallStatus.completed, limit: 100);
await for (final call in calls.autoPaging()) {
print('${call.id} ${call.durationSeconds}s');
}
Page<Lead>? page = await client.leads.list(tag: 'clinic');
while (page != null) {
print('${page.data.length} leads, more: ${page.hasMore}');
page = await page.nextPage();
}
}
In a Flutter file, Page is also a Flutter class. Hide one of them if you need the type: import 'package:wixzel_voice/wixzel_voice.dart' hide Page;.
Phone setup #
Connect a carrier #
The quickest way is the dashboard. Open SIP trunks, pick Twilio, Telnyx, Plivo, Vonage, Exotel or Vobiz under Connect a carrier, and paste your carrier's API keys. It creates the trunk at your carrier for you. A trunk made this way has carrier set, and canBuyNumbers when you can buy numbers through it.
Any other SIP carrier works too. Create the trunk with its details:
Future<void> connectTrunk(WixzelVoice client) async {
final trunk = await client.sipTrunks.create(const CreateSipTrunk(
name: 'My carrier',
host: 'sip.example-carrier.com',
username: 'wixzel',
password: 'your-sip-password',
providerName: 'example-carrier',
));
// Two things to set at your carrier:
print('Allow calls from ${trunk.platformIp}'); // for outbound calls
print('Send inbound calls to ${trunk.originationUri}'); // for inbound calls
// Free and read-only. Start here when calls do not connect.
final status = await client.sipTrunks.status(trunk.id);
print('${status.probe.reachable} ${status.asterisk.detail}');
}
sipTrunks.logs(id) shows recent SIP events, and sipTrunks.test(id) sends a probe to the carrier.
Phone numbers #
A phone number you own at your carrier, registered on a trunk. Give it an inboundAgentId and that agent answers its calls.
Future<void> numbers(WixzelVoice client, String trunkId, String agentId) async {
// Numbers already on the carrier account behind a trunk you connected in one step.
final onAccount = await client.sipTrunks.listCarrierNumbers(trunkId);
for (final n in onAccount) {
print('${n.phoneNumber} attached: ${n.attached}');
}
// Attach a number you already own, and have the agent answer it.
final number = await client.phoneNumbers.create(CreatePhoneNumber(
phoneNumber: '+14155550100',
sipTrunkId: trunkId,
inboundAgentId: agentId,
));
// Or point an existing number at another agent.
await client.phoneNumbers.update(number.id, UpdatePhoneNumber(inboundAgentId: agentId));
// Every number you have registered.
final all = await client.phoneNumbers.list();
print(all.data.map((n) => n.phoneNumber));
}
Buy a number #
On a trunk with canBuyNumbers, search what your carrier sells and buy one. Your carrier bills you at its price. Wixzel Voice adds nothing.
Future<void> buyNumber(WixzelVoice client, String trunkId, String agentId) async {
final available = await client.sipTrunks.searchAvailableNumbers(
trunkId,
country: 'US',
type: NumberType.local,
areaCode: '415',
);
final pick = available.first;
print('${pick.phoneNumber} ${pick.monthlyCost} ${pick.currency}');
final bought = await client.phoneNumbers.purchase(PurchasePhoneNumber(
sipTrunkId: trunkId,
phoneNumber: pick.phoneNumber,
country: 'US',
type: NumberType.local,
inboundAgentId: agentId,
));
print('${bought.phoneNumber.phoneNumber} ${bought.purchaseStatus}');
}
Buying needs a key with the phone_numbers:purchase scope. A retry never buys twice.
Agents #
Create an agent #
Future<Agent> createAgent(WixzelVoice client) => client.agents.create(CreateAgent(
name: 'Front desk',
systemPrompt: 'You answer calls for Smile Dental. Book check-ups. Keep replies short.',
openingMessage: 'Hello, Smile Dental. How can I help?',
language: 'en-IN',
voice: VoiceConfig.realtimeModel(
const RealtimeConfig(model: 'google/gemini-live', voice: 'Kore'),
),
appointmentBookingEnabled: true,
));
openingMessage is spoken as soon as the call connects. Both it and the prompt can use {{name}} and other lead fields. Change an agent later with agents.update(id, UpdateAgent(...)). Only the fields you set change.
Choose the voice and engine #
An engine is either one realtime model that hears, thinks and speaks, or three parts wired together: speech-to-text, a language model and text-to-speech.
Gemini Live, one realtime model:
final geminiLive = VoiceConfig.realtimeModel(
const RealtimeConfig(model: 'google/gemini-live', voice: 'Charon', language: 'auto'),
);
Sarvam, for Indian languages:
final sarvam = VoiceConfig.composed(
stt: const SttConfig(model: 'sarvam/saaras-v3-realtime', language: 'hi-IN'),
llm: const LlmConfig(model: 'sarvam/sarvam-105b'),
tts: const TtsConfig(model: 'sarvam/bulbul-v3', voice: 'shubh'),
);
Classic, any speech-to-text, model and voice you choose:
final classic = VoiceConfig.composed(
stt: const SttConfig(model: 'deepgram/nova-3', language: 'en-US'),
llm: const LlmConfig(model: 'openrouter/gpt-4o-mini', temperature: 0.7),
tts: const TtsConfig(model: 'elevenlabs/eleven_flash_v2_5'),
turnTaking: const TurnTaking(interruptSensitivity: InterruptSensitivity.normal),
);
Do not guess model ids. Ask the API what it can serve right now, which voices an engine has and which languages it holds a conversation in:
Future<void> pickVoice(WixzelVoice client) async {
final engines = await client.engines.list();
for (final engine in engines.data.where((e) => e.available)) {
print('${engine.id} (${engine.kind.value})');
}
final voices = await client.engines.voices('sarvam');
print(voices.data.map((v) => '${v.id}: ${v.name}'));
final languages = await client.engines.languages('gemini_live');
print(languages.data.map((l) => '${l.code}: ${l.name}'));
}
Generate an agent from a website #
Give it a website, a description, or both. It reads the site and writes the prompt, the knowledge base and a mascot in the site's colours. It comes back at once while it builds, so wait for it:
Future<Agent> generateAgent(WixzelVoice client) async {
var agent = await client.agents.generate(const GenerateAgent(
websiteUrl: 'https://smiledental.example.com',
role: 'receptionist',
));
while (agent.mascot.building) {
print(agent.mascot.statusDetail); // reading_website, designing, writing_knowledge, saving
await Future<void>.delayed(const Duration(seconds: 3));
agent = await client.agents.retrieve(agent.id);
}
if (agent.mascot.status == 'failed') {
throw StateError('${agent.mascot.errorCode}: ${agent.mascot.errorMessage}');
}
return agent;
}
Mascots #
Every agent has a mascot: one shape, two eyes and a smile, drawn live from a small JSON spec. It shows on the agent's share page, in the website widget, in the Wixzel Voice app, and in your own Flutter app. While the agent talks, its mouth moves with the voice.
The default look #
A new agent gets the default mascot: a lime squircle with dark round eyes. It is in agent.mascot.spec:
void defaultLook(Agent agent) {
final spec = agent.mascot.spec;
print(spec['name']); // Wix
print(spec['color']); // #a0d425
print(spec['shape']); // {kind: squircle}
}
To start an agent with another look, pass mascot: AgentMascotInput(spec: ...) to CreateAgent.
Customize it #

Send a new spec. It replaces the old one. The API checks it and clamps anything out of range.
Future<void> customize(WixzelVoice client, String agentId) async {
final agent = await client.agents.updateMascot(agentId, const UpdateAgentMascot(
spec: {
'format': 'wixzel.mascot',
'version': 1,
'name': 'Coral',
'color': '#ff6a5c',
'shape': {'kind': 'blob', 'seed': 42},
'eyes': {'style': 'pill', 'color': '#ffffff'},
'personality': {'bounce': 0.8, 'energy': 0.6},
},
welcomeMessage: 'Hi! Tap me to talk.',
));
print(agent.mascot.spec['name']);
}
Shapes are circle, drop, triangle, cloud, pill, flower, squircle, blob, heart and custom. Eye styles are pill, round, block and line.
A new look #
Ask for a new look from a website, a description, or neither for a random one. The agent's prompt does not change.
Future<void> newLook(WixzelVoice client, String agentId) async {
var agent = await client.agents.generateMascot(
agentId,
const GenerateAgentMascot(description: 'A calm, friendly dentist. Soft blue.'),
);
while (agent.mascot.building) {
await Future<void>.delayed(const Duration(seconds: 2));
agent = await client.agents.retrieve(agentId);
}
}
Share it as a link or a website widget #

Turn on the share link, the website widget, or both. Anyone can then call the agent from them, and the calls are billed to you, so set limits. caller_info says what a caller gives first: none, contact (a name and a phone number or email) or google (a Google sign-in).
Future<void> share(WixzelVoice client, String agentId) async {
final agent = await client.agents.updateMascot(agentId, const UpdateAgentMascot(
sharing: {
'link': true,
'embed': true,
'embed_origins': ['https://smiledental.example.com'],
'caller_info': 'contact',
'max_call_seconds': 300,
'daily_minutes': 60,
'calls_per_visitor_daily': 3,
},
));
final id = agent.mascot.publicId;
print('Share link: https://voice.wixzel.com/m?id=$id');
print('<script src="https://voice.wixzel.com/embed.js" data-mascot="$id" async></script>');
}
Turning sharing on needs a key with the calls:write scope. agents.rotateMascotPublicId(id) gives a new share id; the old link and widget code stop working at once.
Draw it in a Flutter app #

The wixzel_voice_flutter package draws the mascot in your app. It is the same runtime the Wixzel Voice app uses, so it looks and moves the same as on the web.
import 'package:flutter/widgets.dart';
import 'package:wixzel_voice/wixzel_voice.dart';
import 'package:wixzel_voice_flutter/wixzel_voice_flutter.dart';
Widget agentFace(Agent agent) => AgentMascotView(agent: agent, size: 200);
A tap plays an expression, and its eyes follow the phone's tilt. MascotController makes it play moves (hello, nod, dance and more), look somewhere or change state.
Lip-sync to a live call #
During a realtime talk, MascotRealtimeLink from wixzel_voice_flutter moves the mascot with the call. It listens while the user talks, thinks while the agent prepares an answer, and moves its mouth in time with the agent's audio.
MascotRealtimeLink follow(RealtimeConnection conn, MascotController mascot) =>
MascotRealtimeLink(mascot, conn);
Pass the same mascot controller to AgentMascotView(agent: agent, controller: mascot). Call link.dispose() when the talk ends. See the wixzel_voice_flutter README for exact timing with your audio player.
Calls #
Place a call #
calls.create dials a number over your carrier and connects an agent. It spends credit, and it is safe to retry.
Future<void> placeCall(WixzelVoice client, String agentId, String numberId) async {
final call = await client.calls.create(CreateCall(
to: '+919876543210',
agentId: agentId,
fromNumberId: numberId, // optional: the agent's own outbound number by default
leadName: 'Asha',
metadata: {'order_id': '1042'},
));
print('${call.id} ${call.status.value}');
}
A test call rings a number and says one line, then hangs up. Use it to check the carrier and hear the voice:
Future<void> testCall(WixzelVoice client, String agentId) async {
final call = await client.agents.testCall(agentId, const TestCall(to: '+919876543210'));
print(call.status.value);
}
List calls, read transcripts, hang up #
Future<void> readCalls(WixzelVoice client, String agentId) async {
final calls = await client.calls.list(agentId: agentId, limit: 20);
for (final call in calls.data) {
print('${call.id} ${call.direction.value} ${call.status.value} ${call.durationSeconds}s');
}
final detail = await client.calls.retrieve(calls.data.first.id);
print('${detail.summary} cost: ${detail.costMicros} micro-USD');
for (final line in detail.transcript) {
print('${line.role.value}: ${line.content}');
}
if (detail.failureReason != null) print('Failed: ${detail.failureReason}');
// Just the transcript.
final transcript = await client.calls.transcript(detail.id);
print(transcript.data.length);
// End a live call. Billing stops when it ends.
await client.calls.hangup(detail.id);
}
calls.delete(id) removes a call's log, transcript and recording.
Talk without a phone number #
An agent can talk inside your app or website with no carrier and no number. Audio goes over a WebSocket as G.711 µ-law, 8 kHz mono, in both directions.
1. On your server, mint a session. It needs your API key, so it never runs in the app:
Future<Map<String, Object?>> mintSession(WixzelVoice client, String agentId) async {
final session = await client.realtime.createSession(CreateRealtimeSession(
agentId: agentId,
maxDurationSeconds: 300,
metadata: {'user_id': '42'},
));
// Send these two to the app. The secret works once, for one minute.
return {'url': session.url, 'client_secret': session.clientSecret};
}
2. In the app, connect, play the agent and send the microphone. The SDK speaks the protocol. Recording and playback are your app's: use any recorder that gives 16-bit PCM at 8 kHz, and any PCM player.
Future<void> talk(String url, String secret) async {
final conn = RealtimeConnection.connect(url: url, clientSecret: secret);
await conn.ready;
conn.events.listen((event) {
switch (event) {
case RealtimeStarted(:final callId):
print('Live. Logged as call $callId');
case RealtimeAudio(:final mulaw):
player.feed(mulawToPcm16(mulaw)); // your PCM player, 8 kHz mono
case RealtimeAudioClear():
player.clear(); // the user spoke over the agent: drop queued audio
case RealtimeTranscript(:final role, :final text):
print('$role: $text');
case RealtimeWarning(:final code, :final secondsRemaining):
print('$code, about $secondsRemaining s left');
case RealtimeError(:final code, :final message):
print('$code: $message');
case RealtimeEnded(:final reason, :final durationSeconds):
print('Ended ($reason) after $durationSeconds s');
default:
}
});
// Your recorder: 16-bit PCM, 8 kHz mono, ideally 160 samples (20 ms) a frame.
mic.listen((Int16List frame) => conn.sendAudio(pcm16ToMulaw(frame)));
// Later, to hang up:
await conn.end();
print(conn.closeCode); // 1000 normally; 4402 means no credit, 4409 too many at once
}
Turn on your platform's echo cancellation. Without it the agent hears itself through the speaker and interrupts itself. The session is billed and logged like a call, with channel set to web. In Flutter, add the mascot so the user has a face to talk to.
Campaigns and leads #
A lead is a person to call. A campaign is one agent calling a list of leads.
Future<void> campaign(WixzelVoice client, String agentId) async {
final result = await client.leads.bulkCreate(const BulkCreateLeads([
CreateLead(name: 'Asha', phoneNumber: '+919876543210', tags: ['clinic'], fields: {'last_visit': 'March'}),
CreateLead(name: 'Ravi', phoneNumber: '+919876543211', tags: ['clinic']),
]));
print('${result.createdCount} added, ${result.failedCount} failed');
final campaign = await client.campaigns.create(CreateCampaign(
name: 'Check-up reminders',
agentId: agentId,
leadIds: result.data.map((l) => l.id).toList(),
));
// Creating does not dial anyone. This does, and spends credit.
await client.campaigns.start(campaign.id);
final now = await client.campaigns.retrieve(campaign.id);
print('${now.status.value} ${now.leadCount} leads');
// client.campaigns.pause(campaign.id) stops new calls and hangs up live ones.
}
Lead fields work as merge fields: a prompt can say {{last_visit}}.
Appointments #
With appointmentBookingEnabled: true on the agent, it books appointments during calls. You can also manage them yourself:
Future<void> appointments(WixzelVoice client, String agentId, String leadId) async {
final booked = await client.appointments.create(CreateAppointment(
phoneNumber: '+919876543210',
dateTime: DateTime.utc(2026, 11, 3, 10, 30),
agentId: agentId,
leadId: leadId,
clientName: 'Asha',
durationMinutes: 30,
));
await client.appointments.update(booked.id, const UpdateAppointment(notes: 'Bring X-rays'));
final upcoming = await client.appointments.list();
for (final a in upcoming.data) {
print('${a.dateTime} ${a.clientName} ${a.status.value}');
}
}
Callers #

A caller is one person who called one agent: from its share link, its website widget, the app or by phone. You see their name and contact (when caller_info asks for them), their approximate location and their device.
Future<void> callers(WixzelVoice client, String agentId) async {
final page = await client.callers.list(agentId: agentId);
for (final c in page.data) {
print('${c.name ?? c.phone ?? 'Unknown'} ${c.kind.value} '
'${c.location.city}, ${c.location.country} '
'${c.platform.os} ${c.platform.browser} calls: ${c.callCount}');
}
// Block someone. They cannot call this agent again from anywhere.
final caller = page.data.first;
await client.callers.update(caller.id, const UpdateCaller(blocked: true));
// The blocklist, and unblocking.
final blocked = await client.callers.list(agentId: agentId, blocked: true);
print(blocked.data.length);
await client.callers.update(caller.id, const UpdateCaller(blocked: false));
// One caller's calls. Each call also carries call.caller.
final theirCalls = await client.calls.list(callerId: caller.id);
print(theirCalls.data.map((c) => c.caller?.platform.source?.value));
}
Knowledge bases #
Facts and FAQs an agent uses during calls. Write one, import it from a website, or add documents it can search.
Future<void> knowledge(WixzelVoice client, String agentId) async {
// Read a website into a draft. Nothing is saved until you create it.
final draft = await client.knowledgeBases.importFromWebsite('https://smiledental.example.com');
final kb = await client.knowledgeBases.create(draft.toCreate());
// Or write it yourself.
await client.knowledgeBases.update(kb.id, const UpdateKnowledgeBase(faqs: [
KnowledgeBaseFaq(question: 'Are you open on Sunday?', answer: 'No, Monday to Saturday, 9 to 6.'),
]));
// Add documents (PDF, Word, text), up to 5 at a time.
final files = await client.knowledgeBases.addFiles(kb.id, [
KnowledgeDocument(bytes: await File('price-list.pdf').readAsBytes(), filename: 'price-list.pdf'),
]);
print(files.first.status); // processing, then ready
await client.agents.update(agentId, UpdateAgent(knowledgeBaseId: kb.id));
}
Tools and MCP integrations #
Agents can act during a call. A tool is your own endpoint the agent calls with arguments. An MCP server is another app's tools, such as a calendar or a CRM.
Future<void> tools(WixzelVoice client, String agentId) async {
final tool = await client.tools.create(const CreateTool.webhook(
name: 'check_order',
description: 'Look up an order by its number and say where it is.',
url: 'https://api.example.com/orders/lookup',
parameters: {
'type': 'object',
'properties': {'order_number': {'type': 'string'}},
'required': ['order_number'],
},
));
final run = await client.tools.test(tool.id, arguments: {'order_number': '1042'});
print(run.ok);
final crm = await client.mcpServers.create(const CreateMcpServer(
name: 'CRM',
url: 'https://mcp.example-crm.com/mcp',
auth: McpServerAuth(type: 'headers', headers: [
McpServerHeader(name: 'Authorization', value: 'Bearer crm-token'),
]),
));
print(crm.tools.map((t) => t.name));
await client.agents.update(agentId, UpdateAgent(toolIds: [tool.id], mcpServerIds: [crm.id]));
}
For a server that signs in with OAuth, create it with McpServerAuth(type: 'oauth'), then open the authorizationUrl from mcpServers.startOAuth(id).
Cloned voices #
Clone one person's voice from recordings, then speak in it. It charges a one-off fee. consent: true records that the voice is yours, or that the speaker agreed.
Future<void> cloneVoice(WixzelVoice client, String agentId) async {
final voice = await client.voices.create(CreateVoice.files(
name: 'Dr. Meera, front desk',
provider: VoiceProvider.sarvam, // or elevenlabs (the default)
language: 'ml-IN', // required for Sarvam
consent: true,
files: [VoiceSample(bytes: await File('meera.mp3').readAsBytes(), filename: 'meera.mp3')],
));
// Or CreateVoice.urls(..., audioUrls: ['https://example.com/meera.mp3'])
// A short mp3 of the voice. Not charged.
final Uint8List clip = await client.voices.preview(voice.id, language: 'hi-IN');
print(clip.length);
// Speak in it, on one of voice.engines.
await client.agents.update(agentId, UpdateAgent(
voice: VoiceConfig.composed(
stt: const SttConfig(model: 'sarvam/saaras-v3-realtime', language: 'ml-IN'),
llm: const LlmConfig(model: 'sarvam/sarvam-105b'),
tts: TtsConfig(model: 'sarvam/bulbul-v3', voice: voice.providerVoiceId),
),
));
}
The create is not retried after an unclear failure, since it spends credit. List the voices before trying again.
Webhooks #
Wixzel Voice sends events to one URL of yours: a call started or finished, a lead qualified, an appointment booked.
Future<void> webhooks(WixzelVoice client) async {
await client.webhooks.update(const UpdateWebhook(
url: 'https://api.example.com/wixzel-events',
enabled: true,
events: [WebhookEvent.callCompleted, WebhookEvent.appointmentBooked],
));
final secret = await client.webhooks.rotateSecret();
print(secret.secret); // shown once; use it to check signatures
final delivery = await client.webhooks.test(WebhookEvent.callCompleted);
print('${delivery.responseStatus} in ${delivery.durationMs} ms');
final recent = await client.webhooks.deliveries(limit: 10);
print(recent.data.map((d) => '${d.event.value} ${d.status}'));
}
Usage and balance #
Future<void> money(WixzelVoice client, String callId) async {
final balance = await client.billing.balance();
print('${balance.balanceDisplay}, available: ${balance.availableMicros} micro-USD');
final month = await client.usage.summary(start: DateTime.utc(2026, 10, 1));
print('Spent ${month.totalDisplay}');
for (final line in month.byComponent) {
print('${line.component.value} ${line.model}: ${line.priceMicros}');
}
// Exactly what one call cost, line by line.
final call = await client.calls.retrieve(callId);
final lines = await client.usage.events(sessionId: call.sessionId);
print(lines.data.map((e) => '${e.component.value} ${e.priceMicros}'));
// Add credit. A person opens the checkout link and pays.
final topup = await client.billing.createTopup(const CreateTopup(amountUsd: 20));
print(topup.checkoutUrl);
// Every movement on the balance.
final ledger = await client.billing.ledger(limit: 20);
print(ledger.data.map((e) => '${e.kind.value} ${e.amountMicros}'));
}
Engines and pricing #
engines.list() is the price list. Each engine lists its models and what each costs per unit, in micro-USD. An engine whose provider is having trouble shows available: false before calls start failing.
Future<void> pricing(WixzelVoice client) async {
final engines = await client.engines.list();
for (final engine in engines.data) {
print('${engine.id} available: ${engine.available}');
for (final m in engine.models) {
print(' ${m.component.value} ${m.model}: ${m.priceMicros} micro-USD per ${m.unit}');
}
}
}
Using it with AI coding agents #
Wixzel Voice has an MCP server. With it, Claude Code, Codex and Cursor can create agents, connect carriers, read calls and place test calls for you, with one tool per API endpoint. Tools that dial or spend money are marked, so the agent asks first.
Claude Code:
claude mcp add --transport http wixzel-voice https://mcp.voice.wixzel.com/mcp
Then type /mcp in Claude Code and choose Authenticate.
Codex:
codex mcp add wixzel-voice --url https://mcp.voice.wixzel.com/mcp
codex mcp login wixzel-voice
Cursor: add this to .cursor/mcp.json:
{
"mcpServers": {
"wixzel-voice": {
"command": "npx",
"args": ["-y", "wixzel-voice-mcp"],
"env": { "WIXZEL_API_KEY": "wv_live_..." }
}
}
}
See the MCP server docs for scopes and other clients.
Every resource and its methods #
Every /v1 endpoint has a method. A test checks this list against the API's OpenAPI document.
| Resource | Methods |
|---|---|
client.agents |
list, create, retrieve, update, delete, testCall, generate, updateMascot, generateMascot, rotateMascotPublicId |
client.calls |
create, createWithResponse, list, retrieve, delete, hangup, transcript |
client.callers |
list, retrieve, update |
client.realtime |
createSession (then RealtimeConnection in the app) |
client.leads |
list, create, bulkCreate, retrieve, update, delete |
client.campaigns |
list, create, retrieve, delete, start, pause |
client.appointments |
list, create, retrieve, update, delete |
client.knowledgeBases |
list, create, importFromWebsite, importFile, listFiles, addFiles, addFileFromUrl, retrieveFile, deleteFile, retrieve, update, delete |
client.tools |
list, create, retrieve, update, delete, test |
client.mcpServers |
list, create, retrieve, update, delete, refresh, startOAuth |
client.voices |
list, create, retrieve, preview, update, delete |
client.phoneNumbers |
list, create, purchase, retrieve, update, delete |
client.sipTrunks |
list, create, retrieve, update, delete, status, logs, test, listCarrierNumbers, searchAvailableNumbers |
client.engines |
list, languages, voices |
client.usage |
events, summary |
client.billing |
balance, ledger, createTopup, createTopupWithResponse |
client.webhooks |
retrieve, update, rotateSecret, test, deliveries |
client.apiKeys |
list, create, revoke, rotate |
addFileFromUrl sends to the same endpoint as addFiles, with a link instead of bytes.
Up to 0.3.0 this package was published as wixzel_phone. To move across, depend on wixzel_voice and import package:wixzel_voice/wixzel_voice.dart. WixzelPhone still works as a deprecated name for WixzelVoice. See the changelog.
Links #
- Documentation and the API reference
- Dashboard
wixzel_voice_flutter: the mascot in your Flutter appwixzel-voice: the same SDK for TypeScript and JavaScriptwixzel-voice-mcp: the MCP server- Source, where issues and pull requests are read
License #
MIT. See LICENSE.