-->

dev ai, llm

Large Language Models have become central to modern artificial intelligence, powering everything from chatbots to code generation tools. Yet for many, they remain mysterious black boxes. This post breaks down how LLMs work, from their fundamental architecture to why they’re so remarkably capable.

What Is an LLM?

A Large Language Model is a type of neural network trained to predict the next token (usually a word or subword) in a sequence. The term “large” refers to both the model’s architecture and its training data. Modern LLMs contain billions of parameters—adjustable weights that shape how information flows through the network—and are trained on trillions of tokens of text from diverse internet sources.

The core task sounds deceptively simple: given a sequence of words, predict what comes next. Yet this seemingly elementary objective, applied at massive scale with sophisticated architecture, produces systems capable of reasoning, coding, translation, and creative writing.

The Transformer Architecture

The breakthrough that enabled modern LLMs was the introduction of the Transformer architecture in 2017. Unlike earlier approaches like recurrent neural networks (RNNs), Transformers use a mechanism called attention to process sequences of text.

The attention mechanism allows the model to examine relationships between all words in an input simultaneously, rather than processing them sequentially. When the model encounters the word “bank,” attention helps it determine whether this refers to a financial institution or the side of a river based on context from the entire input. This parallel processing dramatically improved both training efficiency and model performance.

Transformers are built from stacked layers of attention and feed-forward neural networks. Each layer refines its understanding of the input, learning to extract increasingly abstract features. Early layers might recognize simple patterns like parts of speech, while deeper layers understand semantic relationships and complex reasoning.

graph TB
    A[Input Text: Tokens] --> B[Embedding Layer]
    B --> C[Transformer Layer 1]
    C --> D[Attention Mechanism]
    D --> E[Feed-Forward Network]
    E --> F[Transformer Layer 2]
    F --> G[Attention Mechanism]
    G --> H[Feed-Forward Network]
    H --> I[...]
    I --> J[Final Layer]
    J --> K[Output: Next Token Prediction]

    style D fill:#2ECC40
    style G fill:#2ECC40
    style E fill:#0074D9
    style H fill:#0074D9

Training: From Text to Intelligence

LLM training happens in two main phases: pre-training and fine-tuning.

During pre-training, models are exposed to enormous quantities of text—books, websites, code repositories, scientific papers—and learn to predict the next token. This self-supervised learning requires no manually labeled data; the objective is built into the task itself. Through this process, the model absorbs patterns about language, factual knowledge, reasoning patterns, and how ideas connect.

Pre-training is computationally expensive, requiring specialized hardware like GPUs or TPUs and taking weeks or months. After pre-training, the base model is remarkably capable but still rough around the edges.

Fine-tuning comes next. Here, models are trained on smaller, curated datasets with human feedback. Techniques like Reinforcement Learning from Human Feedback (RLHF) help align model outputs with human preferences. Fine-tuning reduces harmful outputs, improves instruction-following, and makes models more helpful and honest.

graph LR
    A[Raw Text Data<br/>Trillions of Tokens] --> B[Pre-training<br/>Next Token Prediction]
    B --> C[Base Model<br/>Raw Capabilities]
    C --> D[Supervised Fine-tuning<br/>Curated Examples]
    D --> E[RLHF<br/>Human Feedback]
    E --> F[Aligned Model<br/>Helpful & Safe]

    style B fill:#FF851B
    style D fill:#0074D9
    style E fill:#2ECC40

Why They’re So Capable

The capability of modern LLMs emerges from scale, architecture, and training data. Research has shown that performance improves predictably as models grow larger and train on more data—a phenomenon called scaling laws. With enough parameters and training data, these models develop unexpected abilities, sometimes called emergent capabilities.

For instance, LLMs weren’t explicitly programmed to write code, summarize text, or translate languages. Yet with sufficient scale, they spontaneously developed these skills. Few-shot learning is another emergent ability: models can adapt to new tasks with just a few examples, rather than requiring retraining.

This happens because language encodes knowledge about the world. When an LLM learns that “Paris is in France” appears frequently in training data, it internalizes this relationship. Scaling and diverse training data compound this effect, enabling models to handle complex reasoning, creative tasks, and specialized domains.

graph TD
    A[Scale: Parameters + Data] --> B[Basic Language Understanding]
    B --> C[Emergent Capabilities]
    C --> D[Code Generation]
    C --> E[Translation]
    C --> F[Few-shot Learning]
    C --> G[Complex Reasoning]
    C --> H[Creative Writing]

    style A fill:#B10DC9
    style C fill:#FF851B
    style D fill:#2ECC40
    style E fill:#2ECC40
    style F fill:#2ECC40
    style G fill:#2ECC40
    style H fill:#2ECC40

The Limitations

Understanding what LLMs cannot do is equally important. Despite their sophistication, they have fundamental limitations:

LLMs are pattern-matching systems, not reasoning engines. They can produce plausible-sounding text that is factually incorrect—a phenomenon called hallucination. They cannot access real-time information or maintain true long-term memory across conversations. Their outputs reflect biases present in training data. They sometimes struggle with novel problems that don’t match learned patterns.

Additionally, LLMs have finite context windows—maximum amounts of text they can process at once. This limits their ability to handle very long documents or maintain extended conversations.

Looking Forward

LLMs represent a significant step forward in AI, but they’re not the final answer. Researchers are exploring hybrid approaches combining LLMs with retrieval systems, symbolic reasoning, and other techniques to address current limitations. The field continues to evolve rapidly, with improvements in efficiency, alignment, and capability.

Understanding how LLMs work—their strengths and limitations—is essential for anyone working with or relying on modern AI systems. They’re powerful tools, not oracles, and using them effectively requires realistic expectations about their nature and capabilities.

dev aws, lambda, dotnet, webhooks, twilio

This article was originally published on the Twilio Blog.

In this article, you will learn how to develop a web API with .NET 6 to handle Twilio webhooks and deploy it to AWS Lambda. You will also learn how to save call recordings to AWS S3 as MP3 files.

Prerequisites

What are webhooks?

In today’s API-driven world, integrating applications is easier than ever. Most of the time, you can get the information you need from an external system’s API, but sometimes you want to be notified by the external system when something happens. That’s where webhooks come in. You register your own endpoint with the external system, and they post data to your endpoint when the event you’re looking for occurs.

Twilio Webhooks

The type of data you can expect from webhooks depends on the Twilio service. For the Twilio Voice API, there are several types of webhooks, three of which you’ll use in this tutorial:

  • Incoming voice call

  • Status callback

  • Recording status callback

Incoming voice call webhook, as the name implies, is where you handle the incoming calls. When you use a programmable voice service such as Twilio, this is the core functionality you would want to implement. If you don’t handle the incoming call, you hear three beeps when you call your Twilio number, and the call is terminated. The call doesn’t even appear in the logs. As you will see later in this article, when you implement a call handler, you can provide instructions to Twilio to record the call, play audio, and more. Some of these actions also have their own follow up webhooks. These instructions are implemented in TwiML (the Twilio Markup Language). TwiML is an XML-based markup language that has elements such as Say (read text to the caller), Dial (add another party to the call) and Record (record the caller’s voice). You will use Say and Record in your project later.

After a call has been completed (inbound or outbound), Twilio sends an HTTP request to your endpoint. This is called the status callback.

You could receive the recording status callback if you requested to record the call. Then, Twilio sends your endpoint a message with the recording status and a URL to access the recording file. You have to specify your webhook URL to handle this callback message.

!!!warning

By default, Recording URLs don’t require authentication, and recordings are not encrypted. However, you can require basic authentication to access the recordings and configure recordings to be encrypted in the voice settings (Voice → Settings → General).

!!!

In this tutorial you will interact with the Twilio Voice product, but many other products also use webhooks and you can apply the same technique for them as you will for Voice.

Now that you’ve learned about these three webhooks, let’s move on to the next section.

Set up AWS IAM User

You will need credentials to deploy your application to AWS from the command line. To create the credentials, follow the steps below:

First, go to the AWS IAM Users Dashboard and click the Add users button.

Enter the user name such as twilio-webhook-user and tick the Access key - Programmatic access checkbox:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 1

Click the Next: Permissions button at the bottom right.

Then, select Attach existing policies directly and select AdministratorAccess:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 2

Click the Next: Tags button at the bottom right. Tags are optional (and quite valuable information), and it’s a good practice to add descriptive tags to the resources you create. Since this is a demo project, you can skip this step and click the Next: Review button at the bottom.

Confirm your selection on the review page. It should look like this:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 3

Then, click the Create user button.

In the final step of the user creation process, you should see your credentials for the first and the last time.

!!!warning

Take note of your Access key ID and Secret access key before you press the close button.

!!!

Now, open a terminal window and run the following command:

aws configure

You should see a prompt for AWS Access Key ID. Copy and paste your access key ID and press enter.

Then, copy and paste your secret access key and press enter.

Respond to Twilio Webhooks using AWS Lambda and .NET - image 4

When prompted, type us-east-1 as the default region name and press enter.

!!!info

In this example, I will use the us-east-1 region. Regions are geographical locations where AWS have their data centers. It is a good practice to deploy as close to your customers as possible for production deployments to reduce latency. Since this is a demo project, you can use us-east-1 for convenience as it’s the default region in AWS Management Console. You can find more on AWS regions in this document: Regions and Availability Zones.

!!!

As the default output format, type json and press enter.

To confirm you have configured your AWS profile correctly, run the following command:

aws configure list

The output should look like this:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 5

Now that you have set up your AWS credentials, you can move on to setting up the code.

Create an ASP.NET Core project for AWS Lambda

You can download the finished project from GitHub. However, this article will provide step-by-step instructions to set it up yourself.

Open a terminal and navigate to the directory that will be the root of your project.

You will use the Lambda ASP.NET Core Web API project template in the sample project. So, first, install Lambda templates by running the following command:

dotnet new -i Amazon.Lambda.Templates

You should see the results of a successful installation:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 6

Take note of the short name of Lambda ASP.NET Core Web API: serverless.AspNetCoreWebAPI.

Then, run the following command to create the project:

dotnet new serverless.AspNetCoreWebAPI --name TwilioWebhookLambda.WebApi --output .

The command above will create a new project with the following file structure:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 7

Note that the template creates a folder named src and puts the project in that folder. You can move the code to your root folder, but the rest of the article will use the default paths.

You will leverage a new AWS Lambda feature called Function URLs to make the function publicly available. For this to work with your API you need to install the Amazon.Lambda.AspNetCoreServer.Hosting NuGet package. In the terminal window, navigate to the project folder and run:

cd src/TwilioWebhookLambda.WebApi
dotnet add package Amazon.Lambda.AspNetCoreServer.Hosting

Then, open Startup.cs in your IDE, update the ConfigureServices method so that it looks like this:

```csharp hl_lines=”4” public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddAWSLambdaHosting(LambdaEventSource.HttpApi); }


Lambda Function URLs use HttpApi behind the scenes, so you need to use `LambdaEventSource.HttpApi` as the event source type.

You will need the Amazon Lambda Tools .NET tool to deploy the function via the command line. You can install it by running the command below:

```bash
dotnet tool install -g Amazon.Lambda.Tools

Amazon Lambda Tools, use the aws-lambda-tools-defaults.json file to get some details about the installation. Unfortunately, it doesn’t come with all the values it needs. For example, you can store the runtime and the function’s name in this file, so you don’t have to keep entering it whenever you deploy it from scratch.

Open the file and update it so that it looks like this:

{
  "profile": "",
  "region": "",
  "configuration": "Release",
  "function-runtime": "dotnet6",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "TwilioWebhookLambda.WebApi",
  "function-name": "TwilioWebhookLambda-WebApi",
  "function-url-enable": true
}

If you don’t provide profile and region values, it uses the default profile and region in your AWS configuration. If you want to override the defaults, update those values as well.

Then, deploy the Lambda function by running the following command:

dotnet lambda deploy-function

Your Lambda function needs an IAM role to execute. The policies attached to this role determine the permissions of the function. By default, the Amazon Lambda Tools create a role on your behalf and attach it to the function.

During the deployment, it lists the existing roles along with an option to create a new role:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 8

Select the Create new IAM Role option.

Give it a descriptive name, such as TwilioWebhookLambda-WebApi-Role, so that you can easily determine its purpose when you see it in your IAM dashboard.

The next step is to select the IAM policy. Your project will need Amazon S3 access to store call recordings. Also, having access to CloudWatch logs is always helpful. So choose 3 - AWSLambdaExecute from the list:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 9

!!!info

As a best practice, you should develop custom policies to grant only the minimum required permissions.

!!!

After the deployment has finished, you should see the successful deployment message:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 10

The publicly available URL shown above is only created because you enabled the Function URL feature in the aws-lambda-tools-defaults.json file.

"function-url-enable": true

Without this feature, you wouldn’t be able to use a Lambda function as a webhook handler.

Now open that URL in a browser and you should see the default GET / endpoint result:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 11

The API works like any other API. This template comes with a sample controller called ValuesController. Test the controller by appending /api/values to your function URL:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 12

You should see an array of strings (value1 and value2) displayed on your browser.

You just deployed your ASP.NET Core web API to Lambda and made it publicly available. Great job!

Receive Incoming Calls

The Twilio .NET SDK and the helper library for ASP.NET make it easier to build Twilio applications. In this tutorial, you’ll use the SDK to generate TwiML and the helper library to respond to webhook requests. Add the SDK and helper library via NuGet:

dotnet add package Twilio
dotnet add package Twilio.AspNet.Core

Under the Controllers folder, add a new file called IncomingCallController.cs and replace its contents with the following code:

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
using Twilio.TwiML;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class IncomingCallController : TwilioController
{
    [HttpPost]
    public TwiMLResult Index()
    {
        var response = new VoiceResponse();
        response.Say("Hello. Please leave a message after the beep.");
        return TwiML(response);    
    }
}

In the terminal, deploy the updated function:

dotnet lambda deploy-function

You should get a successful update message on your screen:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 13

At this point, you have a publicly available endpoint but Twilio is not aware of it yet.

Go to the Twilio console. Select your account, and then click Phone Numbers → Manage → Active Numbers on the left pane. (If Phone Numbers isn’t on the left pane, click on Explore Products and then on Phone Numbers.)

Respond to Twilio Webhooks using AWS Lambda and .NET - image 14

!!!warning

You don’t permanently own Twilio numbers; instead, you lease them until you release them. If you release a number after a 10-day grace period, it is returned to the number pool.

!!!

Click on the phone number you want to use for your project and scroll down to the Voice section.

Under the “A Call Comes In” label, set the dropdown to Webhook, the text field next to it to your Lambda Function URL suffixed with the /IncomingCall path, the next dropdown to HTTP POST, and click Save. It should look like this:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 15

To test, call your Twilio number, and you should hear the message “Hello. Please leave a message after the beep.”. It doesn’t actually wait for the message, but at least you know you have implemented an incoming voice webhook. Your code is executed when your Twilio number receives a call.

In the next section, you will handle the second webhook type: Call Status Updates.

Receive Call Status Updates

Create a new file in the Controllers folder called CallStatusChangeController.cs with the code below:

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class CallStatusChangeController : TwilioController
{
    private readonly ILogger<CallStatusChangeController> _logger;
    public CallStatusChangeController(ILogger<CallStatusChangeController> logger)
    {
        _logger = logger;
    }
    [HttpPost]
    public async Task Index()
    {
        var form = await Request.ReadFormAsync();
        var to = form["To"];
        var callStatus = form["CallStatus"];
        var fromCountry = form["FromCountry"];
        var duration = form["Duration"];
        _logger.LogInformation(
            "Message to {to} changed to {callStatus}. (from country: {fromCountry}, duration: {duration})",
            to, callStatus, fromCountry, duration);
    }
}

This code logs some of the values posted to your webhook.

Go back to the active number configuration in the Twilio Console and update the “Call Status Changes” field with your Lambda Function URL suffixed with /CallStatusChange, as shown below:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 16

Save your configuration, and then deploy your project again using dotnet lambda deploy-function.

Now call your Twilio number again, and after the call has been completed, you should see the callback logs in CloudWatch:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 17

You received this message when the call status changed to “completed”.

You can also use the Twilio console to view all call logs: Click Monitor → Calls on the left pane.

Locate the call in the list and click the Call SID link to view the details.

Respond to Twilio Webhooks using AWS Lambda and .NET - image 18

In the Request Inspector section, you can see all the callbacks with their requests and responses in detail:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 19

Next, you will look into the third and final type of voice webhook: Recording Status Updates.

Receive Recording Status Updates

!!!warning

Before you record anything, please make sure to read this article: Legal Considerations with Recording Voice and Video Communications.

!!!

Create a new controller called RecordingStatusChangeController and replace its contents with the code below:

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class RecordingStatusChangeController : TwilioController
{
    private readonly ILogger<RecordingStatusChangeController> _logger;
    public RecordingStatusChangeController(ILogger<RecordingStatusChangeController> logger)
    {
        _logger = logger;
    }
    [HttpPost]
    public async Task Index()
    {
        var form = await Request.ReadFormAsync();
        var callSid = form["CallSid"];
        var recordingStatus = form["RecordingStatus"];
        var recordingUrl = form["RecordingUrl"];
        _logger.LogInformation(
            "Recording status changed to {recordingStatus} for call {callSid}. Recording is available at {recordingUrl}"
            ,recordingStatus, callSid, recordingUrl);
    }
}

Similar to the status change handler, this code only logs some request details. Once you’ve seen all webhooks are working fine, you will update the implementation with more meaningful code.

You also need to modify the IncomingCallController and replace the code in the Index method as below:

var response = new VoiceResponse();
response.Say("Hello. Please leave a message after the beep.");
response.Record(
    timeout: 10, 
    recordingStatusCallback: new Uri("/api/RecordingStatusChange", UriKind.Relative)
);
return TwiML(response);

Now you’re telling Twilio that you’d like to record the phone call. You are also specifying the webhook URL that will receive the recording status update. Unlike the other webhook types, there is no field in the Twilio console to set the recording status callback.

Deploy this update and call your number again. This time you should be able to leave a message after the beep. Once you’ve done that, check your CloudWatch logs, and you should see two status updates: One for the call status and one for the recording status:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 20

As you can see in the logs, the recording URLs are public by default, but the recordings have long random names, so they cannot be iterated through and downloaded by unauthorized parties. To increase the security of the recordings, you can enable Enforce HTTP Auth on Media URLs and Voice Recording Encryption options in Voice Settings in your account.

Save Recording MP3 files to an Amazon S3 Bucket

Now let’s see how you can retrieve the recording file and upload it to an Amazon S3 bucket from your ASP.NET Core project.

!!!info

As of May 2022, Twilio has a built-in feature to store recordings in an Amazon S3 bucket. In this article, however, you will use a different approach and upload the MP3 files programmatically from your Lambda function.

!!!

First, you will need an S3 bucket to store the files. To create the bucket, go to AWS Management Console and search for S3:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 21

Then, click the link to go to the S3 service dashboard.

Click Create Bucket button:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 22

Give it a descriptive and globally unique name, accept all the defaults and click the Create Bucket button at the bottom of the screen.

You should see your bucket in the bucket list:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 23

!!!info

Amazon S3 bucket names are global. If somebody else created a bucket named my-twilio-call-recordings, you can not also use that name. You can find more bucket naming rules in AWS Documentation.

!!!

In your application, you need to install the AWS SDK packages to talk to the Amazon S3 API.

In the terminal, run the following command:

dotnet add package AWSSDK.S3 

Update the RecordingStatusChangeController code as below:

using Amazon.S3;
using Amazon.S3.Transfer;
using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class RecordingStatusChangeController : TwilioController
{
    [HttpPost]
    public async Task Index()
    {
        string recordingUrl = Request.Form["RecordingUrl"];
        string fileName = $"{recordingUrl.Substring(recordingUrl.LastIndexOf("/") + 1)}.mp3";
        string bucketName = "my-twilio-call-recordings";
        using HttpClient client = new HttpClient(); // use HttpClient factory in production
        using HttpResponseMessage response = await client.GetAsync($"{recordingUrl}.mp3");
        using Stream recordingFileStream = await response.Content.ReadAsStreamAsync();
        using var s3Client = new AmazonS3Client();
        using var transferUtility = new TransferUtility(s3Client);
        await transferUtility.UploadAsync(recordingFileStream, bucketName, fileName);
    }
}

Make sure to set the bucketName with your bucket’s name.

In this code, when you receive the recording URL, you extract the recording file name, retrieve the file content via a stream and pass the stream to S3 TransferUtility which uploads it to the Amazon S3 bucket as {fileName}.mp3.

!!!info

By default, the recording URL doesn’t have a file extension. If you call the URL as is, Twilio returns the WAV version of the recording. To get the MP3 version, you need to append .mp3 to the URL as shown in client.GetAsync call.

!!!

During the setup process, you didn’t explicitly tell AWS that your Lambda function should have access to your S3 bucket. So you might be wondering how you have permission to do that. The reason is you chose AWSLambdaExecute policy to be attached to your function’s role. So if you go to the IAM dashboard and search AWSLambdaExecute, you should see the policy’s permissions are defined like this:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 24

You can see that this policy has permission to put objects into all S3 buckets. Since this is a demo project, I decided to keep things simple. However, in production, I’d recommend writing your own policy and giving the minimum required permissions, such as using the names of the resources instead of using wildcards. You can read more on that here: IAM Best Practices: Apply least-privilege permissions

Deploy the final version of the API and call your number again.

A short while after you’ve completed the call, you should see the recording in your bucket:

Respond to Twilio Webhooks using AWS Lambda and .NET - image 25

!!!warning

As the focus of this article is using AWS Lambda to respond to Twilio webhooks, securing your endpoint is not covered in this article. To learn more about Webhooks Security, you can read this article: Webhooks Security.

!!!

Conclusion

Congratulations! You covered three types of webhooks for the Twilio Voice service and implemented handlers for all of them. In addition, you managed to download recordings to your own storage. Later on, you can download or move the files to cold storage using Amazon S3 Glacier. The possibilities are endless when you can manage all of this programmatically. For example, you could use Amazon Transcribe to transcribe the call recording to text, or you could use Twilio’s transcribe attribute on the record-verb.

If you didn’t follow along and implement the project, don’t worry. You can always download the final project from my GitHub repository and experiment on your own.

If you enjoyed playing with call recordings and webhooks using Twilio API, I’d recommend you take a look at these articles as well:

dev aws, polly, voice, dotnet, twilio

This article was originally published on the Twilio Blog.

Technology provides us with countless benefits, and as programmers, we should better ourselves to produce more value all the time. But then again, there are certain seasons in a year when you should just relax and have fun with your skills. In this tutorial, you will use Amazon Polly to create audio files from text, add some optional (ideally spooky) sound effects and play that file to your friends using Twilio Voice and .NET.

!!!warning

Needless to say, all this is meant to be is just some harmless fun between you and your loved ones. Do NOT use it if you’re not sure it will be well-received by the person you call. In a production scenario, make your users opt-in before sending them text messages or automated phone calls.

!!!

Prerequisites

You’ll need the following things in this tutorial:

Project Overview

Let’s take a look at how the application will work.

  • You come up with a message that will sound scary/ominous. You can personalize it by adding your friend’s name or something private in the message to increase the impact.

  • You create an audio file using Amazon Polly based on your crafted text.

  • Optional (but recommended as it makes the experience more fun), you use ffmpeg to add some effects to your audio to add more spookiness.

  • You upload the final audio to Amazon S3.

  • You call your friend and play the audio by using Twilio Voice.

Now that you understand how the application will work let’s get started.

Set up AWS IAM User

You will need credentials to deploy your application to AWS from the command line. To create the credentials, follow the steps below:

First, go to the AWS IAM Users Dashboard and click the Add users button.

Enter the user name, such as twilio-webhook-user and tick the Access key - Programmatic access checkbox:

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 1

Click the Next: Permissions button at the bottom right.

Then, select Attach existing policies directly and select AdministratorAccess:

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 2

Click the Next: Tags button at the bottom right. Tags are optional (and quite valuable information), and it’s a good practice to add descriptive tags to the resources you create. Since this is a demo project, you can skip this step and click the Next: Review button at the bottom.

Confirm your selection on the review page. It should look like this:

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 3

Then, click the Create user button.

In the final step of the user creation process, you should see your credentials for the first and the last time.

!!!warning

Take note of your Access key ID and Secret access key before you press the close button.

!!!

Now, open a terminal window and run the following command:

aws configure

You should see a prompt for AWS Access Key ID. Copy and paste your access key ID and press enter.

Then, copy and paste your secret access key and press enter.

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 4

When prompted, type us-east-1 as the default region name and press enter.

!!!info

In this example, I will use the us-east-1 region. Regions are geographical locations where AWS have their data centers. It is a good practice to deploy as close to your customers as possible for production deployments to reduce latency. Since this is a demo project, you can use us-east-1 for convenience as it’s the default region in AWS Management Console. You can find more on AWS regions in this document: Regions and Availability Zones.

!!!

As the default output format, type json and press enter.

To confirm you have configured your AWS profile correctly, run the following command:

aws configure list

The output should look like this:

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 5

Now that you have set up your AWS credentials, you can move on to setting up the code.

Create S3 Bucket to Store MP3 Files

The audio files you create and modify locally won’t be accessible for Twilio to play. To fix that issue, you will need a public storage area. In this tutorial, you will use an Amazon S3 bucket for storage.

In a terminal window, run the following command to create a new bucket:

aws s3api create-bucket --bucket  {Your bucket name}

!!!info

{Your bucket name} has to follow these bucket naming rules documented by AWS.

!!!

This bucket will be used by Amazon Polly to store text-to-speech outputs. Also, you’re going to upload modified audio files to this bucket as well.

For Polly to store outputs in this bucket, you need to give write permissions to it. This can be achieved by setting the bucket policy as below:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::{Your bucket name}",
        "arn:aws:s3:::{Your bucket name}/*"
      ]
    }
  ]
}

Store the above JSON in a file such as policy.json and replace {Your bucket name} with the actual name of your S3 bucket.

Then, run the following command to update permissions on the bucket:

aws s3api put-bucket-policy --bucket {Your bucket name} --policy file://policy.json

!!!warning

This policy is too broad, and I’d not recommend it for a serious project, but for a quick and fun project such as this one, it’s fine. After you’ve finished this tutorial, make sure to delete the contents and the bucket itself.

!!!

Create Audio Messages with Amazon Polly

First, let’s focus on preparing the audio message. At this point, you should have an idea of your message to convert to audio.

Start by creating a console application:

mkdir SpookySeasonPrankPrepareAudio
cd SpookySeasonPrankPrepareAudio
dotnet new console

The easiest way to use AWS APIs with C# is to use their .NET SDK. They have a modular structure so you can only include the libraries for the services you will use. In this section, you will use Amazon Polly so go ahead and run the following command to include that package only:

dotnet add package AWSSDK.Polly

Open the project in your IDE and replace the contents of Program.cs with the code below:

using Amazon.Polly;
using Amazon.Polly.Model;
using (var pollyClient = new AmazonPollyClient())
{
    var startSpeechSynthesisTaskRequest = new StartSpeechSynthesisTaskRequest
    {
        Text = "James! I know what you did last summer!. You're not getting away with it this time!",
        VoiceId = VoiceId.Matthew,
        OutputFormat = OutputFormat.Mp3,
        OutputS3BucketName = "{Your bucket name}",
        OutputS3KeyPrefix = "polly-output",
    };
    await pollyClient.StartSpeechSynthesisTaskAsync(startSpeechSynthesisTaskRequest);
}

This message is crafted for my imaginary friend James. I don’t know anybody named James, so it’s just a test name that I used. Tailor your spooky message to achieve your own “evil” goal! 🎃

You can also change the voice that Amazon Polly is going to use. Again, there are no right or wrong answers here. Just play around with the options and use the ones you like.

Run the application using dotnet run.

Now open Amazon Polly dashboard, and you should see your synthesis task:

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 6

It also shows the URL of the output file. Download the file by using AWS CLI:

aws s3api get-object --bucket {Your bucket name} --key polly-output.{Your task ID}.mp3 polly-output-original.mp3

Replace the placeholders for the bucket name and task ID. You can use any name for the local file name.

You can find the MP3 files on the project’s GitHub repository.

Now that you have a semi-spooky customized audio message, move on to the next section to add effects to spook it up a notch!

Audio Effects with ffmpeg

I’m by no means an expert in audio mixing and editing. So if you are blessed with those skills, feel free to do your thing and move on to the next section.

In this tutorial, you will add a rather spooky background to the previous audio generated by Amazon Polly.

One good resource for finding free assets is pixabay.com. The sound clip I used in this tutorial is called Halloween Impact 05 by Charlie Raven. Open the link in a new tab and the download should start automatically. Alternatively, you can go to pixabay.com directly, search for the name of the audio and click the Download button next to it.

Copy the downloaded file next to the Polly-generated file (polly-output-original.mp3) and run the following command in a terminal window in the same folder:

ffmpeg -i polly-output-original.mp3 -i halloween-impact-05-93808.mp3 -filter_complex "[1:a]adelay=2s:all=1[a1];[0:a][a1]amix=inputs=2[a]" -map "[a]" output.mp3

The command above merges both files and adds a 2-second delay to the background audio. This delay is to avoid the loud sound drowning out the name.

This is where you can get creative and experiment with various combinations to produce the scariest results. Once you’re happy with the result, upload it your S3 bucket by running the command below:

aws s3api put-object --bucket {Your bucket name} --key output.mp3 --body output.mp3 --content-type audio/mpeg

Create a Console Application to Use Twilio Voice

Now that you have your final audio ready, the last step is to call your “victim” and play it.

To achieve this, create a new console application.

Back in your terminal window, run the following commands:

cd ..
mkdir SpookySeasonPrank
cd SpookySeasonPrank
dotnet new console

Then, while still in the terminal, add Twilio .NET SDK via NuGet:

dotnet add package Twilio

Twilio SDK makes it easy to generate TwiML and interact with Twilio API.

To make phone calls using Twilio API, you will need your Account Sid and Auth Token, which you can obtain from the Twilio Console.

Open the Twilio Console and log in to your account.

In the welcome screen, you should see your Account SID and Auth Token at the bottom in the Account Info section:

Make a Spooky Phone Call using Twilio Voice and Amazon Polly - image 7

You can use environment variables or a vault service to store these values, but for local development, you can use dotnet user secrets by running the following command:

dotnet user-secrets init

Then, add your Account SID and Auth Token by replacing the placeholders with actual values and running the commands below:

dotnet user-secrets set Twilio:AccountSid {Your Account Sid}
dotnet user-secrets set Twilio:AuthToken {Your auth Token}

Storing them in user secrets would not help much if you didn’t have a way to retrieve them. To get the values back, you’re going to use the .NET configuration builder which is in Microsoft.Extensions.Configuration NuGet package.

In the terminal, add the following configuration extension libraries:

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets

You can also add command line or environment variable providers, but you won’t use them in this example.

Open the project with your IDE and update Program.cs with the code below:

using Microsoft.Extensions.Configuration;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
IConfiguration config = new ConfigurationBuilder()
    .AddUserSecrets<Program>(optional: true, reloadOnChange: false)
    .Build();
var twilioAccountSid = config["Twilio:AccountSid"];
var twilioAuthToken = config["Twilio:AuthToken"];
TwilioClient.Init(twilioAccountSid, twilioAuthToken);
CallResource.Create(
    url: new Uri("https://{Your bucket name}.s3.amazonaws.com/output.mp3"),
    method: Twilio.Http.HttpMethod.Get, 
    to: new Twilio.Types.PhoneNumber("{Your victim's phone number}"),
    from: new Twilio.Types.PhoneNumber("{Your Twilio Phone Number}")
);

Replace {Your bucket name}, {Your victim’s phone number} and {Your Twilio Phone Number} with the actual values. You can obtain your Twilio phone number from Twilio Console (It should be shown right below your Account SID and Auth Token).

I’d recommend testing the application with your phone first. Once you’ve confirmed your message sounds as scary as you wanted to sound, you can replace your number with your victim’s phone number and execute your evil plan by running:

dotnet run

Happy Spooky Season!

Conclusion

I hope you enjoyed following this tutorial as much as I enjoyed writing it. Having programmatic access to phone calls and SMS messages opens many possibilities. Combined with other cloud services, you can quickly create a wide variety of projects. If you enjoy developing with Twilio and .NET, you might want to take a look at these articles too: