IVR toolkit
The IVR toolkit is CallFactory’s developer-facing action library for building custom call flows. Each action — Answer, Play, Dial, Record, StartStream, and more — gives you precise control over what happens during a call.
Use it to build phone menus, collect caller input, stream audio to AI services, connect callers to agents, and handle every edge case. The toolkit runs on the same IVR platform that powers all CallFactory features.
- Structured actions with clear signatures and examples
- Stream audio to AI, STT, or custom endpoints
- Build complete call flows without extra hardware
- Full documentation and developer support
Benefits of the IVR toolkit
See how the IVR toolkit helps development teams build powerful call flows.
Full control over call logic
Define exactly what happens at every step of a call. Play prompts, collect input, route based on conditions, stream to AI, and handle errors — all with predictable, well-documented actions.
Built for developers
Every action has a clear signature, typed return values, error handling, and working code examples. Build with confidence using structured actions that fit naturally into your backend.
AI-ready audio streaming
Stream live call audio to any WebSocket endpoint — an AI assistant, a speech-to-text engine, or your own analytics service. StartStream and StopStream give you full control over when and where audio flows.
Runs on proven infrastructure
The toolkit runs on CallFactory's own IVR platform — the same infrastructure that powers all our telephony features. No extra hardware, no third-party dependencies. Works with your existing CallFactory numbers.
IVR Actions Toolkit
Click on an action to view its description, signature and examples.
Answers an incoming call. Must be called before playing audio or collecting input on an inbound call.
Description
- Marks the call as answered on the telephony side.
- Required before using actions like
Play,PromptDigit,GatherDigits,Dial,Record, etc., on inbound calls.
Throws
InvalidOperationException- if the call has already been answered.
Signature
void Answer();Example
protected override async Task
HandleCallAsync(CancellationToken ct)
{
// Always answer first on inbound calls
Answer();
await Play("welcome.wav", ct);
}
Plays an audio file to the caller or to an outbound channel.
Description
- Plays an audio file (e.g.,
.wav) located in the server’s audio directory. - Can target the inbound caller or a specific OutboundChannel.
Parameters
audioFile- File name/path relative to the IVR audio directory.channel- (optional) Outbound channel to play audio to.ct- Cancellation token; cancelled when the caller or channel disconnects.
Returns
PlayResult.Success- Audio played fully.PlayResult.Fail- Playback failed (e.g., invalid file).PlayResult.Cancel- Operation cancelled (e.g., caller hung up).PlayResult.Error- Unexpected error during playback.
Throws
OperationCanceledException- If ct is cancelled during playback.- Other transport/IO exceptions depending on implementation.
Signatures
Task<PlayResult> Play(
string audioFile,
CancellationToken ct = default);
Task<PlayResult> Play(
string audioFile,
OutboundChannel channel,
CancellationToken ct = default);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
Answer();
var result = await Play("welcome.wav", ct);
if (result != PlayResult.Success)
{
Logger.LogWarning("Failed to play welcome message (Result:
{Result})", result); return;
}
await Play("next_prompt.wav", ct);
}
Plays an audio prompt and collects a single DTMF digit.
Description
- Plays a menu prompt (e.g., ‘Press 1 for sales, 2 for support, 3 to leave a message.’).
- Waits for a single DTMF digit: 0-9, *, or #.
- Intended for main menu selections.
Parameters
audioFile- Prompt file to play.timeoutSeconds- How long to wait for a digit (default 10).ct- Cancellation token; canceled when the caller disconnects.
Returns
MenuResult.Successwith Digit set when a digit is received.MenuResult.Timeoutwhen no digit is received withintimeoutSeconds.MenuResult.Cancelwhen operation is cancelled.
Throws
OperationCanceledException- If ct is cancelled (e.g., caller hangs up).
Signatures
Task<(MenuResult Result, char? Digit)> PromptDigit(
string audioFile,
int timeoutSeconds = 10,
CancellationToken ct = default);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
Answer();
await Play("welcome.wav", ct);
var (menuResult, digit) = await PromptDigit(
"main_menu.wav",
timeoutSeconds: 10,
ct);
if (menuResult == MenuResult.Success && digit.HasValue)
{
switch (digit.Value)
{
case '1':
await HandleSales(ct);
break;
case '2':
await HandleSupport(ct);
break;
default:
await Play("invalid_option.wav", ct);
await Hangup(ct);
break;
}
}
else if (menuResult == MenuResult.Timeout)
{
await Play("no_input_goodbye.wav", ct);
await Hangup(ct);
}
else
{
await Play("system_error.wav", ct);
await Hangup(ct);
}
}
Plays a prompt and collects multiple DTMF digits (e.g., account number, PIN).
Description
- Plays a prompt asking the caller to enter several digits.
- Stops when either:
maxDigitsis reached
- A termination digit (e.g., #) is pressed
- Timeout expires
Parameters
audioFile– Prompt to play (e.g., “Please enter your account number followed by #”).maxDigits– Maximum digits to collect before stopping.terminationDigits– String of digits that end collection when entered.timeoutSeconds– Maximum time to wait for input.ct– Cancellation token.
Returns
- Tuple (
GatherResult Result,string? Digits): GatherResult.Successand Digits set when input is collected.GatherResult.Timeoutwhen no input received.GatherResult.Cancelwhen operation is cancelled.GatherResult.Erroron unexpected error.
Throws
OperationCanceledException- If ct is cancelled (caller hangs up).
Signatures
Task<(GatherResult Result, string? Digits)> GatherDigits(
string audioFile,
int maxDigits = 20,
string terminationDigits = "#",
int timeoutSeconds = 30,
CancellationToken ct = default);
Example
protected override async Task
HandleCallAsync(CancellationToken ct)
{
Answer();
await Play("welcome.wav", ct);
var (result, digits) = await GatherDigits(
"enter_account.wav",
maxDigits: 10,
terminationDigits: "#",
timeoutSeconds: 30,
ct);
if (result == GatherResult.Success && !string.IsNullOrEmpty(digits))
{
await ProcessAccountNumber(digits, ct);
}
else if (result == GatherResult.Timeout)
{
await Play("no_input_goodbye.wav", ct);
await Hangup(ct);
}
else
{
await Play("system_error.wav", ct);
await Hangup(ct);
}
}
Dials one or more outbound phone numbers and returns an OutboundChannel when answered.
Description
- Initiates an outbound call to a single destination or to multiple destinations in parallel.
- For multiple destinations, the first to answer wins; all others are cancelled.
Parameters
destination/destinations– Phone number(s) to dial.callerId– Number to present as Caller ID.ringTimeoutSeconds– Maximum time to ring before giving up.ct– Cancellation token.
Returns
- Single destination:
(DialerResult Result, OutboundChannel? Channel)- Multiple destinations:
(DialerResult Result, string? AnsweredDestination, OutboundChannel? Channel)DialerResultmay be: Init, Ringing, Answered, Busy, Rejected, NoAnswer, Failed, Cancel.
Throws
OperationCanceledException– If the operation is cancelled while dialing.
Signatures
Task<(DialerResult Result, OutboundChannel? Channel)> Dial(
string destination,
string callerId,
int ringTimeoutSeconds = 60,
CancellationToken ct = default);
Task<(DialerResult Result, string? AnsweredDestination,
OutboundChannel? Channel)> Dial(
string[] destinations,
string callerId,
int ringTimeoutSeconds = 40,
CancellationToken ct = default);
Example (single destination)
private async Task TransferToSupport(CancellationToken ct)
{
var (dialResult, channel) = await Dial(
destination: "18885554444",
callerId: Context.Ani,
ringTimeoutSeconds: 30,
ct);
if (dialResult == DialerResult.Answered && channel != null)
{
await Play("connecting_to_support.wav", ct);
await Connect(channel, ct);
}
else
{
await Play("support_unavailable.wav", ct);
await HandleVoicemail(ct);
}
}
Example (multiple destinations)
private async Task TransferToSalesHuntGroup(CancellationToken ct)
{
var salesTeam = new[]
{
"18885551111",
"18885552222",
"18885553333"
};
var (result, answeredNumber, channel) = await Dial(
destinations: salesTeam,
callerId: Context.Ani,
ringTimeoutSeconds: 40,
ct);
if (result == DialerResult.Answered && channel != null)
{
Logger.LogInformation("Connected to sales agent {Number}", answeredNumber);
await Connect(channel, ct);
}
else
{
await Play("sales_unavailable.wav", ct);
await HandleVoicemail(ct);
}
}
Bridges audio between two channels.
Description
- For inbound flows: bridges the inbound caller to an outbound channel.
- For outbound-only scenarios: bridges two outbound channels together.
- Blocks until one side hangs up or the connection is otherwise terminated.
Parameters
channel– Outbound channel to connect to the inbound call.primary,secondary– Two outbound channels to bridge.ct– Cancellation token.
Returns
ConnectResult.Success– Connection ended normally (call terminated).ConnectResult.Error– Connection could not be established or failed.
Throws
OperationCanceledException– If ct is cancelled while connected.
Signatures
Task<ConnectResult> Connect(
OutboundChannel channel,
CancellationToken ct = default);
Task<ConnectResult> Connect(
OutboundChannel primary,
OutboundChannel secondary,
CancellationToken ct = default);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
Answer();
await Play("connecting_you_now.wav", ct);
var (dialResult, channel) = await Dial(
destination: "18885550000",
callerId: Context.Ani,
ringTimeoutSeconds: 30,
ct);
if (dialResult == DialerResult.Answered && channel != null)
{
var connectResult = await Connect(channel, ct);
Logger.LogInformation("Connection ended with result: {Result}", connectResult);
}
else
{
await Play("agent_unavailable.wav", ct);
}
await Hangup(ct);
}
Records audio from the caller or an outbound channel.
Description
- Starts recording from either the inbound caller or a specific outbound channel.
- Ends when:
timeLimitSecondsis reached
- A termination digit is pressed (if configured)
- Caller or channel hangs up
Parameters
timeLimitSeconds– Maximum recording length.fileName– Optional custom filename (auto-generated when null).terminationDigits– DTMF digits that stop the recording.playBeep– Whether to play a beep before recording.channel– Optional outbound channel.ct– Cancellation token.
Returns
- Tuple (
RecordResult Result,string? FilePath): RecordResult.SuccesswithFilePathis saved.RecordResult.Timeout,MaxDurationReached,TerminationDigit,Cancel,Error.
Throws
OperationCanceledExceptionif cancelled.
Signatures
Task<(RecordResult Result, string? FilePath)> Record(
int timeLimitSeconds = 120,
string? fileName = null,
string? terminationDigits = null,
bool playBeep = true,
CancellationToken ct = default);
Task<(RecordResult Result, string? FilePath)> Record(
OutboundChannel channel,
int timeLimitSeconds = 120,
string? fileName = null,
string? terminationDigits = null,
bool playBeep = true,
CancellationToken ct = default);
Example
private async Task HandleVoicemail(CancellationToken ct)
{
await Play("leave_message_after_beep.wav", ct);
var (recordResult, filePath) = await Record(
timeLimitSeconds: 180,
fileName: null,
terminationDigits: "#",
playBeep: true,
ct: ct);
if (recordResult == RecordResult.Success && !string.IsNullOrEmpty(filePath))
{
Logger.LogInformation("Voicemail saved at {Path}", filePath);
await Play("thank_you_message_received.wav", ct);
}
else
{
Logger.LogWarning("Recording failed: {Result}", recordResult);
await Play("recording_failed.wav", ct);
}
await Hangup(ct);
}
Rejects an inbound call with a SIP reason code and terminates the call.
Description
- Used for call screening, blocking, and out-of-hours behaviour.
- Returns a SIP error code to the upstream carrier.
Parameters
reason–RejectReason.Busy,.TemporarilyUnavailable,.Declined.ct– Cancellation token.
Returns
RejectResult.Success– Call rejected.RejectResult.AlreadyAnswered– Call already answered.RejectResult.Error– Failed to reject.
Throws
OperationCanceledExceptionif cancelled.
Signatures
Task<RejectResult> Reject(
RejectReason reason = RejectReason.Busy,
CancellationToken ct = default);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
if (IsBlockedNumber(Context.Ani))
{
await Reject(RejectReason.Declined, ct);
return;
}
if (!IsWithinBusinessHours(DateTime.UtcNow))
{
await Reject(RejectReason.TemporarilyUnavailable, ct);
return;
}
// Normal flow
Answer();
await Play("welcome.wav", ct);
}
Cleanly terminates the active call.
Description
- Ends the call from the IVR side.
Returns
HangupResult.Success– Call ended successfully.HangupResult.NotAnswered– Never answered.HangupResult.AlreadyDisconnected– Caller hung up.HangupResult.Error– Hangup failed.
Throws
OperationCanceledExceptionif cancelled.
Signature
Task<HangupResult> Hangup(CancellationToken ct = default);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
Answer();
await Play("goodbye.wav", ct);
var result = await Hangup(ct);
Logger.LogInformation("Hangup result: {Result}", result);
}
Pauses execution for a given number of seconds.
Description
- Waits for durationSeconds while keeping the call open.
- May be interrupted by DTMF input depending on implementation.
Parameters
durationSeconds– Duration in seconds.ct– Cancellation token.
Returns
PauseResult.Success– Pause completed normally.PauseResult.Interrupted– Caller pressed a key during pause (if supported).PauseResult.Cancel– Operation cancelled.PauseResult.Error– Pause failed.
Throws
OperationCanceledException– If ct is cancelled.
Signatures
Task<PauseResult> Pause(
int durationSeconds,
CancellationToken ct = default
);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
Answer();
await Play("please_wait.wav", ct);
var result = await Pause(3, ct);
if (result == PauseResult.Interrupted)
{
await Play("you_pressed_a_key.wav", ct);
}
else
{
await Play("thank_you_for_waiting.wav", ct);
}
await Hangup(ct);
}
Sends a busy signal to the caller and terminates the call.
Description
- Presents a standard busy tone.
- Commonly used when all agents/lines are occupied.
Returns
BusyResult.Success– Busy signal sent and call ended.BusyResult.Cancel– Operation cancelled.BusyResult.Error– Failed to send busy signal or end the call.
Throws
OperationCanceledException– If ct is cancelled.
Signature
Task<BusyResult> Busy(CancellationToken ct = default);
Example
protected override async Task HandleCallAsync(CancellationToken ct)
{
if (AllAgentsBusy())
{
var result = await Busy(ct);
Logger.LogInformation("Busy result: {Result}", result);
return;
}
// Otherwise, proceed with normal flow
Answer();
await Play("welcome.wav", ct);
}
Starts streaming the live audio of the call to an external endpoint (e.g., AI or STT engine).
Description
- Opens a real-time media stream from the call to the given url (e.g., WebSocket endpoint).
- Typically used to send audio to:
- an AI assistant,
- a speech-to-text engine,
- a custom analytics/monitoring service.
- Only one active stream per call is recommended.
Parameters
url– Target streaming endpoint (e.g.,wss://ai.callfactory.nl/voice-stream).options– Optional streaming configuration (direction, metadata, name).ct– Cancellation token. If cancelled, the stream is torn down.
Throws
OperationCanceledException– If ct cancelled during setup.- Connection/transport exceptions depending on implementation.
Signatures
Task<StreamResult> StartStream(
string url,
StreamOptions? options = null,
CancellationToken ct = default
);
Parameters
public class StreamOptions
{
public string? Name { get; set; } //
Optional stream name
public StreamDirection Direction { get; set; } =
StreamDirection.Both;
public Dictionary<string, string>? Metadata { get; set; }
}
public enum StreamDirection
{
Inbound, // From caller to AI
Outbound, // From agent/system to AI
Both
}
Returns
public enum StreamResult
{
Started, // Stream successfully started
Stopped, // Stream successfully stopped (for
StopStream)
AlreadyStarted, // A stream is already active
NotActive, // No active stream (for StopStream)
Error // Failed to start/stop
}
Example
protected override async Task
HandleCallAsync(CancellationToken ct)
{
Answer();
// Start streaming caller audio to AI
var streamResult = await StartStream(
url: "wss://ai.callfactory.nl/voice-stream",
options: new StreamOptions
{
Name = "support-ai",
Direction = StreamDirection.Inbound,
Metadata = new Dictionary<string, string>
{
["caller"] = Context.Ani,
["dnis"] = Context.Dnis
}
},
ct
);
if (streamResult != StreamResult.Started)
{
Logger.LogWarning("Failed to start AI stream:
{Result}", streamResult);
await Play("ai_unavailable.wav", ct);
await Hangup(ct);
return;
}
await Play("connected_to_ai.wav", ct);
// Continue IVR logic while streaming is active...
var (menuResult, digit) = await
PromptDigit("ai_menu.wav", 30, ct);
if (menuResult == MenuResult.Success && digit == '0')
{
// Caller wants a human agent
await StopStream(ct);
await Play("transferring_to_human_agent.wav", ct);
await TransferToHuman(ct);
}
else
{
await Play("thank_you_goodbye.wav", ct);
await StopStream(ct);
await Hangup(ct);
}
}
Stops an active audio stream that was previously started with StartStream.
Description
- Gracefully shuts down the active media stream.
- Does not hang up the call - only stops sending audio.
- Safe to call even if no stream is active (returns
NotActive).
Parameters
ct– Cancellation token.
Returns
StreamResult.Stopped– Stream successfully stopped.StreamResult.NotActive– No active stream found.StreamResult.Error– Failed to stop stream.
Throws
OperationCanceledException– Ifctis cancelled.
Signatures
Task<StreamResult> StopStream(
CancellationToken ct = default);
Example
private async Task TransferToHuman(CancellationToken ct)
{
// Stop AI streaming before transferring to human
var stopResult = await StopStream(ct);
Logger.LogInformation("StopStream result: {Result}",
stopResult);
await Play("transferring_to_agent.wav", ct);
var (dialResult, channel) = await Dial(
destination: "18005550000",
callerId: Context.Ani,
ringTimeoutSeconds: 30,
ct
);
if (dialResult == DialerResult.Answered && channel !=
null)
{
await Connect(channel, ct);
}
else
{
await Play("agent_unavailable.wav", ct);
await Hangup(ct);
}
}
How the IVR toolkit works
The IVR toolkit is a library of actions that your code calls during a live phone call. When a call comes in, your application receives it and decides what to do — step by step — using these actions.
A typical flow starts with Answer(), then plays a welcome message with Play(), collects input with PromptDigit() or GatherDigits(), and routes the caller with Dial() and Connect(). Each action returns a typed result so your code always knows what happened and can handle every outcome.
Audio streaming for AI and speech-to-text
The StartStream and StopStream actions let you stream live call audio to any WebSocket endpoint in real time. This is how you connect a call to an AI assistant, a speech-to-text engine, or a custom analytics service.
You control when streaming starts, which direction the audio flows (caller to AI, agent to AI, or both), and when it stops. The stream runs alongside your other IVR actions — so you can continue playing prompts, collecting input, or transferring the call while the AI processes the audio in the background.
Who uses the IVR toolkit
The toolkit is designed for development teams that need full control over call behaviour. SaaS platforms adding voice features to their product. Contact centres building custom routing logic. Companies integrating telephony with their CRM, ERP, or internal tools.
If your team has developers who can write backend code, the IVR toolkit gives them everything they need to build professional call flows — from simple menus to complex AI-driven conversations. Documentation, code examples, and developer support are included.
The relationship between toolkit and platform features
Every CallFactory feature — phone menus, call queues, voicemail, voicebots — is built on the same IVR actions documented in this toolkit. The toolkit gives you direct access to the same building blocks.
This means anything CallFactory can do, your development team can do too — and more. You can combine standard features with custom logic, add integrations that do not exist as standard features, and build call flows that are entirely unique to your business.
Getting started
Contact us to discuss your requirements. We provide your team with access to the toolkit documentation, code examples, and a development environment. If you need help designing your first call flow, our team can advise on architecture and best practices.
For businesses that need maximum capacity and isolation, the natural upgrade path is dedicated IVR servers — a fully private environment where your custom call flows run on infrastructure reserved exclusively for you.
Learn more about other features
Find more information about our features that can boost your business communications.
Voicebot
AI-powered voice assistants built on our IVR platform. Automate incoming calls with natural conversation, data lookups, and smart routing.
Dedicated IVR servers
Your own IVR server with full control. Build custom call applications, connect AI platforms, and scale from 4 to 200+ channels. The natural upgrade from our private API when you need more.
API integrations
Tell us what you need, we build the endpoints. Our private API lets you integrate CallFactory telephony into your own software — no generic library, no guesswork, no wasted development time.
IVR toolkit FAQ
Get clear answers about the IVR toolkit and how it works for your business.
Yes. The toolkit is designed for development teams that write backend code. Each action has a clear signature and typed return values. If you do not have developers, consider our done-for-you custom telephony or voicebot options instead.
The toolkit uses C# (.NET) as the primary language. All actions, signatures, and examples in the documentation are in C#.
Yes. The StartStream action opens a real-time WebSocket connection to any endpoint — an AI assistant, a speech-to-text engine, or your own service. You control the audio direction and can stop the stream at any time.
Yes. All IVR actions work with your existing CallFactory numbers and routing. No changes to your phone setup are needed.
Yes. The toolkit uses the same building blocks as all CallFactory features. You can use standard features like phone menus and call queues alongside your custom logic.
Every action supports cancellation tokens. When the caller hangs up, the token is cancelled and your code receives a clean signal to stop processing. No orphaned connections or unhandled states.
Yes. Our team can advise on architecture, review your flow design, and help with more advanced integrations. For fully managed solutions, see our custom telephony and voicebot services.
The toolkit itself is available to CallFactory customers. Depending on the complexity of your implementation and hosting requirements, additional fees may apply. We discuss pricing before any work begins.









