Respond to SMS and Voice webhooks using F# and Giraffe

March 28, 2023
Written by
Reviewed by

Respond to  SMS and Voice webhooks  with F# and Giraffe

Twilio's SMS and Voice webhooks let you respond to messages and voice calls using TwiML (the Twilio Markup Language). You can implement these webhooks using any F# web framework, but in this tutorial, you'll use one of the most popular F# web frameworks, Giraffe.

Prerequisites

Here’s what you will need to follow along:

You can find the source code for this tutorial on GitHub. Use it if you run into any issues, or submit an issue if you run into problems.

Set up your F# web project

Open your preferred shell and use the .NET CLI to create a new empty ASP.NET Core project with the F# language argument:

dotnet new web -o GiraffeTwilioWebhooks --language F#
cd GiraffeTwilioWebhooks

Twilio provides some libraries to speed up the development of your Twilio applications, which you'll use for this project. Add the Twilio helper library for .NET and the Twilio helper library for ASP.NET Core using the following commands:

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

Next, add the Giraffe package:

dotnet add package Giraffe

Now you are ready to write some code!

Implement the SMS and Voice webhook

When implementing the SMS or voice webhook, you're expected to respond with TwiML, which instructs Twilio how to respond to a message or call.

Open your project using your preferred IDE and update the Program.fs file with the following code:

module GiraffeTwilioWebhooks.App

open System.Xml.Linq
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open Giraffe
open Twilio.AspNet.Common
open Twilio.TwiML

/// Writes TwiML object to the HTTP response body
let writeTwimlAsync (twiml : TwiML) : HttpHandler = fun (_ : HttpFunc) (ctx : HttpContext) ->
    ctx.SetContentType "application/xml"
    ctx.WriteStringAsync(twiml.ToString(SaveOptions.None))

/// Handles Twilio Messaging webhook requests and responds with Messaging TwiML
let messageHandler (smsRequest: SmsRequest) = MessagingResponse().Message($"Ahoy {smsRequest.From}!") |> writeTwimlAsync
    
/// Handles Twilio Voice webhook requests and responds with Voice TwiML
let voiceHandler = VoiceResponse().Say("Ahoy!") |> writeTwimlAsync

let webApp =
    choose [
        route "/message" >=> bindForm<SmsRequest>(None) messageHandler
        route "/voice"   >=> voiceHandler
    ]

let configureApp (app : IApplicationBuilder) =
    app
        .UseGiraffe webApp

let configureServices (services : IServiceCollection) =
    services
        .AddGiraffe() |> ignore

[<EntryPoint>]
let main _ =
    Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .Configure(configureApp)
                    .ConfigureServices(configureServices)
                    |> ignore)
        .Build()
        .Run()
    0

The code adds Giraffe to ASP.NET Core's services and adds Giraffe to the request pipeline.

The Giraffe webapp routes the HTTP requests with path /message to the messageHandler after binding the form body to an SmsRequest object, and routes HTTP requests with path /voice directly to the voiceHandler.

Both handlers generate TwiML instructions that are written to the HTTP response body using writeTwimlAsync.

Start your application using this command:

dotnet run

Your application will print out the localhost URL(s) the web server is listening to, including the port number. In the upcoming commands, replace [YOUR_LOCALHOST_URL] with one of those localhost URLs.

To quickly test this without configuring your Twilio phone number, use the following cURL commands or PowerShell:

curl -X POST -F From=+12345678901 [YOUR_LOCALHOST_URL]/message
curl -X POST [YOUR_LOCALHOST_URL]/voice

You should get the following TwiML as the responses:

<?xml version="1.0" encoding="utf-8"?>
<Response>
  <Message>Ahoy +12345678901!</Message>
</Response>

Configure the webhooks on your Twilio phone number

To see how this would behave in your Twilio application, you'll need to configure the message and voice webhook on your Twilio phone number. But before you can do that, you'll need to make your locally running application accessible to the internet. You can quickly do this using ngrok which creates a secure tunnel to the internet for you.

Leave your .NET application running, and run the following command in a separate shell:

ngrok http [YOUR_LOCALHOST_URL]

ngrok will print the Forwarding URL, which you'll need to publicly access your local application.

Now, go to the Twilio Console in your browser, use the left navigation to navigate to Phone Numbers > Manage > Active Numbers, and then select the Twilio phone number you want to test with. (If Phone Numbers isn't on the left pane, click Explore Products and then on Phone Numbers.)

Then, on the phone number configuration page, locate the "A call comes in" section. Underneath that:

  • set the first dropdown to Webhook,
  • set the text box next to it to the ngrok forwarding URL — adding on the /voice path,
  • and set the dropdown after the text box to "HTTP POST".

Follow the same steps at the "A message comes in" section within the Message Configuration menu, but use the /message path instead. Then, click Save.

Now, call and text the Twilio phone number. When calling, you should hear "Ahoy". When texting, you should receive a response like "Ahoy [YOUR_PHONE_NUMBER]!".

Secure your webhooks

Now that your web application is publicly accessible, it's also accessible to malicious actors. Luckily, the Twilio helper library for ASP.NET Core has a method RequestValidationHelper.IsValidRequest that validates that the HTTP request originates from Twilio. 

Update Program.fs as below to use it:

module GiraffeTwilioWebhooks.App

open System.Xml.Linq
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.HttpOverrides
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open Giraffe
open Microsoft.Extensions.Options
open Twilio.AspNet.Common
open Twilio.AspNet.Core
open Twilio.TwiML

/// Writes TwiML object to the HTTP response body
let writeTwimlAsync (twiml : TwiML) : HttpHandler = fun (_ : HttpFunc) (ctx : HttpContext) ->
    ctx.SetContentType "application/xml"
    ctx.WriteStringAsync(twiml.ToString(SaveOptions.None))
    
/// Exits out of the request pipeline
let earlyReturn : HttpFunc = Some >> HttpFuncResult.FromResult

/// Validates that the HTTP request originates from Twilio, if not returns a 403 Forbidden response.
let validateTwilioRequest : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        let request = ctx.Request
        let options = ctx.RequestServices
                         .GetRequiredService<IOptionsSnapshot<TwilioRequestValidationOptions>>().Value
        let authToken = options.AuthToken
        let urlOverride =
            if System.String.IsNullOrEmpty(options.BaseUrlOverride)
            then null
            else $"{options.BaseUrlOverride}{request.Path}{request.QueryString}"
        let allowLocal = options.AllowLocal
        
        if RequestValidationHelper.IsValidRequest(ctx, authToken, urlOverride, allowLocal)
        then next ctx
        else setStatusCode 403 earlyReturn ctx
        
/// Handles Twilio Messaging webhook requests and responds with Messaging TwiML
let messageHandler (smsRequest: SmsRequest) = 
    MessagingResponse().Message($"Ahoy {smsRequest.From}!") |> writeTwimlAsync
    
/// Handles Twilio Voice webhook requests and responds with Voice TwiML
let voiceHandler = VoiceResponse().Say("Ahoy!") |> writeTwimlAsync

let webApp =
    validateTwilioRequest >=> choose [
        route "/message" >=> bindForm<SmsRequest>(None) messageHandler
        route "/voice"   >=> voiceHandler
    ]

let configureApp (app : IApplicationBuilder) =
    app
        // Necessary for request validation so the reverse proxy or tunnel URL is used for validation
        .UseForwardedHeaders()
        .UseGiraffe webApp

let configureServices (services : IServiceCollection) =
    services
        .Configure<ForwardedHeadersOptions>(
            fun (options: ForwardedHeadersOptions) -> options.ForwardedHeaders <- ForwardedHeaders.All
        )
        // Configures .NET configuration for Twilio request validation
        .AddTwilioRequestValidation()
        .AddGiraffe() |> ignore

[<EntryPoint>]
let main _ =
    Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .Configure(configureApp)
                    .ConfigureServices(configureServices)
                    |> ignore)
        .Build()
        .Run()
    0

The validateTwilioRequest HTTP handler will validate that the HTTP request originates from Twilio. If the HTTP request originates from Twilio, the next HTTP handler is invoked, otherwise a 403 Forbidden response is sent back.

The new validateTwilioRequest HTTP handler is included in the Giraffe webApp so it is invoked before the /message and /voice routing occurs.

The request validation uses the URL of the incoming request as part of the validation check. The forwarded headers middleware is used to make sure the request URL uses the ngrok forwarding URL and not the localhost URL.

The request validator will use the auth token configured at Twilio:RequestValidation:AuthToken. Since the Auth Token is a secret that you should not share, you should avoid hard-coding it, such as putting it in your appsettings.json file, or any other way it could end up in your source control history. Instead, use the Secrets Manager aka user-secrets, environment variables, or a vault service.

Run the following commands to initialize user-secrets:

dotnet user-secrets init

Then grab the Auth Token from your Twilio account and set it using this command:

dotnet user-secrets set Twilio:RequestValidation:AuthToken [YOUR_AUTH_TOKEN]

When you restart your application, everything should continue to work, but any HTTP request not coming from Twilio or your localhost will be rejected.

Next steps

You learned how to read incoming SMS and calls, how to respond to them, and how to secure your Twilio webhooks, all using F# and Giraffe.

In addition to receiving SMS and responding to incoming SMS, you can also send SMS from your application. Learn how to send SMS from F# using Twilio.

Here are a couple more resources to further your learning on ASP.NET Core and Twilio:

We can't wait to see what you build. Let us know!

Niels Swimberghe is a Belgian American software engineer and technical content creator at Twilio. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ personal blog on .NET, Azure, and web development at swimburger.net.