# Build an AI Voice Agent

Connect inbound calls to an AI voice agent powered by ElevenLabs Conversational AI. The runtime handles SIP bridging; you write the webhook logic that personalizes each conversation.

## How it works

1. Caller dials your Sinch number.
2. Sinch sends an **ICE** callback to your function.
3. Your function responds with a `connectAgent` action targeting ElevenLabs via SIP.
4. Sinch bridges the call to ElevenLabs.
5. (Optional) ElevenLabs sends a **conversation-init** webhook to personalize the greeting.
6. After the call ends, ElevenLabs sends a **post-call** webhook with transcript and analysis.


## Required environment variables

```bash
ELEVENLABS_AGENT_ID=your-agent-id
ELEVENLABS_API_KEY=your-api-key    # for outbound calls and post-call webhooks
```

## Inbound call — connect to agent

C#
```csharp
using Microsoft.AspNetCore.Mvc;
using SinchFunctions.AI;
using SinchFunctions.Models;
using SinchFunctions.Utils;

namespace SinchFunctions
{
    public class FunctionController : SinchVoiceController
    {
        public FunctionController(
            FunctionContext context, IConfiguration configuration,
            ILogger<FunctionController> logger) : base(context, configuration, logger) { }

        public override async Task<IActionResult> Ice([FromBody] IceCallbackModel callbackData)
        {
            var agentId = Configuration["ELEVENLABS_AGENT_ID"]
                ?? throw new InvalidOperationException("ELEVENLABS_AGENT_ID is required");

            return Ok(new IceSvamletBuilder()
                .Instructions.Say("Connecting you to our AI assistant. Please hold.")
                .Action.ConnectAgent(AgentProvider.ElevenLabs, agentId)
                .Build());
        }

        public override async Task<IActionResult> Ace([FromBody] AceCallbackModel callbackData)
            => Ok(new AceSvamletBuilder().Action.Continue().Build());

        public override async Task<IActionResult> Dice([FromBody] DiceCallbackModel callbackData)
        {
            Logger.LogInformation("Call ended: {Reason}, {Duration}s",
                callbackData.Reason, callbackData.Duration);
            return Ok();
        }
    }
}
```

## ConnectAgent options

C#
```csharp
return Ok(new IceSvamletBuilder()
    .Action.ConnectAgent(AgentProvider.ElevenLabs, agentId, new ConnectAgentOptions
    {
        Cli = callbackData.Cli,
        MaxDuration = 1800,
        SuppressCallbacks = false,
    })
    .Build());
```

## IVR gating before agent connection

Show a menu first and connect to the agent only if the caller opts in:

C#
```csharp
public override async Task<IActionResult> Ice([FromBody] IceCallbackModel callbackData)
{
    var menu = MenuFactory.CreateMenu()
        .Prompt("Press 1 to speak with our AI assistant, or press 0 to reach a human agent.")
        .Option("1", "return(ai_agent)")
        .Option("0", "return(human_agent)")
        .Timeout(5000).Repeats(2).Build();

    return Ok(new IceSvamletBuilder().Action.RunMenu(menu).Build());
}

public override async Task<IActionResult> Pie([FromBody] PieCallbackModel callbackData)
{
    var agentId = Configuration["ELEVENLABS_AGENT_ID"]!;
    var humanNumber = Configuration["HUMAN_AGENT_NUMBER"]!;

    return callbackData.MenuResult?.Value switch
    {
        "ai_agent" => Ok(new PieSvamletBuilder()
            .Instructions.Say("Connecting you to our AI assistant.")
            .Action.ConnectAgent(AgentProvider.ElevenLabs, agentId).Build()),
        "human_agent" => Ok(new PieSvamletBuilder()
            .Instructions.Say("Connecting you to a human agent.")
            .Action.ConnectPstn(humanNumber).Build()),
        _ => Ok(new PieSvamletBuilder()
            .Instructions.Say("Invalid selection. Goodbye.")
            .Action.Hangup().Build()),
    };
}
```

## Conversation-init webhook

ElevenLabs calls this webhook before the agent's first word. Return `dynamic_variables` to personalize the session.

Configure the URL in your ElevenLabs agent settings:
`https://<name>.fn.sinch.com/webhook/elevenlabs/conversation-init`

C#
```csharp
using Microsoft.AspNetCore.Mvc;
using SinchFunctions.AI.ElevenLabs;
using SinchFunctions.Controllers;

namespace SinchFunctions
{
    public class MyElevenLabsWebhooksController : ElevenLabsController
    {
        private readonly ILogger<MyElevenLabsWebhooksController> _logger;

        public MyElevenLabsWebhooksController(ILogger<MyElevenLabsWebhooksController> logger)
        { _logger = logger; }

        public override async Task<IActionResult> HandleConversationInitWebhook(
            [FromBody] ConversationInitRequest request)
        {
            return Ok(new ConversationInitResponse
            {
                Type = "conversation_initiation_client_data",
                DynamicVariables = new Dictionary<string, object>
                {
                    ["caller_phone"]   = request.CallerId ?? "unknown",
                    ["customer_name"]  = "valued customer",
                    ["account_tier"]   = "standard",
                }
            });
        }

        public override async Task<IActionResult> HandlePostCallWebhook(
            [FromBody] ElevenLabsPostCallWebhook webhook)
        {
            _logger.LogInformation("Call completed: Id={Id}, Duration={Duration}s",
                webhook.Data?.ConversationId, webhook.Data?.Metadata?.CallDurationSecs);
            return Ok();
        }
    }
}
```

## ConversationInitResponse fields

| Field | Type | Description |
|  --- | --- | --- |
| `dynamic_variables` | `Record<string, string>` | Key-value pairs injected into agent prompt and first message |
| `conversation_config_override.agent.first_message` | `string` | Override opening line |
| `conversation_config_override.agent.prompt` | `string` | Override system prompt |
| `conversation_config_override.agent.language` | `string` | Override language (e.g. `"es"`) |


## Outbound calls via ElevenLabs

ElevenLabs initiates the outbound call. Your function bridges the SIP leg to PSTN:

C#
```csharp
public override async Task<IActionResult> Ice([FromBody] IceCallbackModel callbackData)
{
    if (callbackData.OriginationType == "SIP")
    {
        var destination = callbackData.To?.Endpoint ?? "";
        return Ok(new IceSvamletBuilder()
            .Action.ConnectPstn(destination, cli: callbackData.Cli).Build());
    }
    return Ok(new IceSvamletBuilder().Action.Hangup().Build());
}

public override async Task<IActionResult> Ace([FromBody] AceCallbackModel callbackData)
    => Ok(new AceSvamletBuilder().Action.Continue().Build());
```

For sinch.json configuration and environment variables, see [Templates](/docs/functions/functions/templates) and [Configuration & Secrets](/docs/functions/functions/concepts/configuration-secrets).