You can quickly send single SMS and MMS using Twilio, but when you want to send multiple SMS/MMS to a recipient, Twilio cannot guarantee that the messages will be delivered in the order they were created.
In this tutorial, you'll learn how to guarantee the delivery order of your messages, by creating an application that sends the 4 different parts of the following meme as separate MMS's:
Prerequisites
Here’s what you will need to follow along:
- .NET 7 SDK (earlier and newer versions may work too)
- A code editor or IDE (I recommend JetBrains Rider, Visual Studio, or VS Code with the C# plugin)
- A free Twilio account (sign up with Twilio for free)
- A Twilio phone number
You can find the source code for this tutorial on GitHub. Use it if you run into problems, or submit an issue if you need help.
Create and set up your C# console project
Open your preferred shell and run the following commands to create a new VB.NET console project:
dotnet new console -o SendMultipleSmsInOrder
cd SendMultipleSmsInOrder
You'll need to store sensitive secrets to authenticate with the Twilio API. You'll use the .NET Secret Manager, aka user secrets, to store your secrets.
Start by initializing user secrets for your project using this command:
dotnet user-secrets init
Then, go back to the Twilio Console and find your Account SID and Auth Token in the Account Info section. Use them to replace the respective placeholders in the commands below before running them; they store the Account SID and Auth Token as user secrets.
dotnet user-secrets set "Twilio:AccountSid" "[YOUR_ACCOUNT_SID]"
dotnet user-secrets set "Twilio:AuthToken" "[YOUR_AUTH_TOKEN]"
Next, configure the Twilio phone number you want to send texts from and the phone number you want to send texts to:
dotnet user-secrets set "FromPhoneNumber" "[YOUR_TWILIO_NUMBER]"
dotnet user-secrets set "ToPhoneNumber" "[RECIPIENT_PHONE_NUMBER]"
To load these secrets into your .NET application, you'll need to add the user secrets configuration package. Add it by running the following command:
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
Next, add the Twilio SDK for .NET to your project:
dotnet add package Twilio
Now, open the project in your preferred IDE and update the Program.cs file with the following code:
using Microsoft.Extensions.Configuration;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
var from = configuration["FromPhoneNumber"];
var to = configuration["ToPhoneNumber"];
TwilioClient.Init(
configuration["Twilio:AccountSid"],
configuration["Twilio:AuthToken"]
);
var images = new[]
{
"https://raw.githubusercontent.com/Swimburger/SendMultipleSmsInOrder/main/images/DogMeme1.png",
"https://raw.githubusercontent.com/Swimburger/SendMultipleSmsInOrder/main/images/DogMeme2.png",
"https://raw.githubusercontent.com/Swimburger/SendMultipleSmsInOrder/main/images/DogMeme3.png",
"https://raw.githubusercontent.com/Swimburger/SendMultipleSmsInOrder/main/images/DogMeme4.png",
};
for (var index = 0; index < images.Length; index++)
{
var image = images[index];
var messageResource = await MessageResource.CreateAsync(
to: new PhoneNumber(to),
from: new PhoneNumber(from),
mediaUrl: new List<Uri>
{
new(image)
}
);
Console.WriteLine($"Status: {messageResource.Status}");
}
The program:
- Loads the user secrets for the project and puts them into the
configuration
variable - Retrieves the Twilio Account SID and Auth Token and initializes the
TwilioClient
. - Sends 4 images using 4 separate MMS.
Try it out by running the following command:
dotnet run
The output of this looks like this:
Status: queued
Status: queued
Status: queued
Status: queued
When you create a message resource in Twilio's API, Twilio will not send the message during that API call, and then respond. Instead, it'll create the message resource and queue it for delivery, hence the status is queued
.
Open your phone to see the resulting MMS's arrive at your phone. It should look like this:
All the images have been received successfully, but if you pay close attention, you'll notice that they are completely out of order. While Twilio will send the messages in the order that you’ve queued them, the messages are delivered individually with no association to each other. The order of delivery depends on the carrier and the receiving mobile device.
This isn't just the case with MMS messages, but also with normal SMS. However, sending MMS takes significantly longer, which is why it is much easier to demonstrate this problem with MMS.
Let's take a look at how you can solve this problem.
Add delays between messages
The easiest way to "solve" this is by adding delays between sending each message.
Update the previous for-loop with the following for-loop:
for (var index = 0; index < images.Length; index++)
{
var image = images[index];
var messageResource = await MessageResource.CreateAsync(
to: new PhoneNumber(to),
from: new PhoneNumber(from),
mediaUrl: new List<Uri>
{
new(image)
}
);
Console.WriteLine($"Status: {messageResource.Status}");
// if not the last image
if (index != images.Length - 1)
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
}
Then run the application again:
dotnet run
You'll notice that there's a 10-second delay between each image and the delivery order should be preserved, in most cases.
For SMS, a one-second delay should give you a similar result.
While this works, it's a hacky solution where you're guessing how much delay you should add. You're not really guaranteeing the delivery will be in order, rather you're lowering the chances of them being delivered out of order.
Poll the message status
Rather than guessing how much delay you should add to minimize the problem, you can query the status of the message over and over until it is delivered
. Checking whether data has changed on an interval is also called polling.
Update the for-loop again:
for (var index = 0; index < images.Length; index++)
{
var image = images[index];
var messageResource = await MessageResource.CreateAsync(
to: new PhoneNumber(to),
from: new PhoneNumber(from),
mediaUrl: new List<Uri>
{
new(image)
}
);
Console.WriteLine($"Status: {messageResource.Status}");
// if not the last image
if (index != images.Length - 1)
{
const int amountOfPollingAttempts = 20;
var pollingInterval = TimeSpan.FromSeconds(1);
for (var i = 0; i < amountOfPollingAttempts; i++)
{
messageResource = await MessageResource.FetchAsync(messageResource.Sid);
if (messageResource.Status == MessageResource.StatusEnum.Delivered)
{
break;
}
await Task.Delay(pollingInterval);
}
}
}
In the above code, amountOfPollingAttempts
will determine how many times you will check whether the status has changed to delivered
, and pollingInterval
is the time between each check. In the above example, the status will be checked up to 20 times, with a one-second delay between each check.
MMS delivery is slower depending on the size of the media. When I was testing the application above, it took about 8 polling attempts until the MMS was delivered. SMS on the other hand is usually delivered within a second, when I tested it. Results may vary depending on location and carrier.
Once the status changes to delivered
, the inner for-loop is stopped so that the outer for-loop can move to the next iteration and send the next message. If the status never becomes delivered
in the 20 times it is checked, the outer for-loop will still continue. You may want to change this behavior to log a warning and to stop the outer for-loop as well.
Run the application again to try out the change:
dotnet run
Just like before, you should receive the images in the correct order.
You could further enhance this code by refactoring the polling code into a method. Here's the update for-loop preceded by a local function that takes care of the polling:
async Task<bool> WaitForDelivery(string messageSid, int amountOfPollingAttempts, TimeSpan pollingInterval)
{
for (var i = 0; i < amountOfPollingAttempts; i++)
{
var message = await MessageResource.FetchAsync(messageSid).ConfigureAwait(false);
if (message.Status == MessageResource.StatusEnum.Delivered)
{
return true;
}
await Task.Delay(pollingInterval).ConfigureAwait(false);
}
return false;
}
for (var index = 0; index < images.Length; index++)
{
var image = images[index];
var messageResource = await MessageResource.CreateAsync(
to: new PhoneNumber(to),
from: new PhoneNumber(from),
mediaUrl: new List<Uri>
{
new(image)
}
);
Console.WriteLine($"Status: {messageResource.Status}");
// if not the last image
if (index != images.Length - 1)
{
var wasDelivered = await WaitForDelivery(
messageSid: messageResource.Sid,
amountOfPollingAttempts: 20,
pollingInterval: TimeSpan.FromSeconds(1)
);
if (wasDelivered == false)
{
Console.WriteLine("Message wasn't delivered in time.");
break;
}
}
}
The WaitForDelivery
method performs the same polling logic as before, but returns true
if delivered and false
if not delivered within the expected time. If false
a warning is logged to the console and the for-loop is stopped.
Use the status callback webhook
When you create a message resource, you can set the statusCallback
parameter. This parameter takes a URL and Twilio will send an HTTP request with the message details whenever the status changes. You can use this instead of polling, to get the delivery notification in real-time.
It would be possible to use a web server to receive these statusCallback
requests, and connect it to your console application to get the status changes in real-time. There would be some other ways to accomplish this, but all these solutions are a lot more complicated and won't be covered in this tutorial. I'd recommend keeping it simple and stick with delays or polling.
If you are interested in the statusCallback
webhook, check out the docs for the Message Resource.
Next steps
Great job! You now know how to send multiple SMS and MMS messages while preserving their order. Want to put this new technique to use? Check out this tutorial on building an SMS chatbot using OpenAI's ChatGPT.
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.