How to Build an RCS Business Messaging Campaign with Twilio in C#

September 25, 2026
Written by

Twilio guide on creating an RCS business messaging campaign with icons of communication and tech tools on a dark background.

Rich Communication Services (RCS) is the upgrade to SMS that turns everyday text messages into branded conversations with images, carousels, verified sender logos, suggested replies, and read receipts all delivered natively through the default messaging app on the recipient’s phone.

Twilio Programmable Messaging supports RCS Business Messaging as a first-class channel, which means you can reuse the same API you already use for SMS to deliver rich and interactive messages at scale.

In this tutorial, you’ll learn how to build an RCS Business Messaging campaign in .NET 10 or higher. You’ll register an RCS Sender, design a rich card template in the Twilio Content Template Builder, broadcast the campaign to a list of recipients, track delivery status with a webhook, and capture replies when users tap a suggested reply button.

Prerequisites

RCS Sender registration is a manual review process. Twilio's onboarding guide walks you through submitting your brand details, logo, and use case. While the sender is under review, you can add test devices that will receive messages immediately without waiting for full approval.

How RCS campaigns work with Twilio

Before writing any code, it helps to understand how the pieces fit together:

  • RCS Sender: Your verified brand identity on the RCS network. This is what recipients see at the top of the conversation (your logo, business name, and verified checkmark).
  • Messaging Service: A Twilio resource that groups senders (RCS, SMS, WhatsApp, etc.) together. You send from the Messaging Service and Twilio picks the right sender based on the recipient’s capabilities.
  • Content Template: A reusable message layout with dynamic variables. RCS supports rich cards, carousels, quick replies, and call-to-action buttons.
  • Campaign script: Your .NET code that loops over a recipient list and sends each person a personalized message using the Content Template.
  • Status callback: A webhook that Twilio calls every time a message changes state (queued, sent, delivered, read, failed) so you can measure the campaign in real time.
  • Inbound webhook: A webhook that Twilio calls when a recipient taps a suggested reply or sends a message back, so you can capture responses.

Register an RCS Sender and add it to a Messaging Service

If you haven’t already registered an RCS Sender, do that first. Log in to your Twilio Console and navigate to Products & Services > Numbers & Senders > Overview > RCS. Click Create new Sender and follow the prompts to submit your brand information.

Screenshot of Twilio Numbers & Senders page, focusing on the RCS section with options to create and manage senders.

After adding in the public details,, add your test device to the Tester sender section on the Try it out page so you can start sending immediately.

Twilio interface to create an RCS sender with options to test message and configure settings.

Next, add the RCS Sender to a Messaging Service:

  • Navigate to Configure and scroll down to the Add to a Messaging Service section
  • Click an existing Messaging Service and click Save

Make note of the Messaging Service SID (starts with MG) from the service overview page of your messaging service. You’ll use it in your .NET application.

Step 1: Set up a .NET application

You’ll start setting up your application by initializing a .NET project and installing the dependencies you’ll need.

Initialize a .NET project

Open up your terminal, navigate to your preferred directory for .NET projects, and enter the following commands to create a folder, change into it, and initialize a .NET project:

dotnet new web -n RCSMessager
cd RCSMessager

Install the required dependencies

Next, run the following command to install the nuget packages you’ll need for this project:

dotnet add package Twilio
dotnet add package DotNetEnv
  • twilio: The official Twilio .NET helper library.
  • dotenv: Loads environment variables from a .env file so credentials stay out of your source code.

Import environment variables

You’ll need your Twilio Account SID, Auth Token, and Messaging Service SID to send messages. Log in to your Twilio Console and locate the Account SID and Auth Token on the homepage.

Front page of the Twilio Console showing the Account Info section with the Account SID and Auth Token.
Front page of the Twilio Console showing the Account Info section with the Account SID and Auth Token.

Head back to your IDE and create a file named .env in the root of the project. Copy the following into your .env file:

TWILIO_ACCOUNT_SID=XXXXXX
TWILIO_AUTH_TOKEN=XXXXXX
MESSAGING_SERVICE_SID=XXXXXX
STATUS_CALLBACK_URL=XXXXXX

Replace each XXXXXX placeholder with the corresponding value:

  • TWILIO_ACCOUNT_SID: Your Account SID from the Twilio Console.
  • TWILIO_AUTH_TOKEN: Your Auth Token from the Twilio Console.
  • MESSAGING_SERVICE_SID: The SID of the Messaging Service that you connected with your RCS Sender.
  • STATUS_CALLBACK_URL: Leave this blank for now. You’ll fill it in once you start ngrok in a later step.

Save this file.

Step 2: Create a rich RCS Content Template

RCS is at its best when the message includes a hero image, a headline, a body, and suggested reply buttons. You’ll create the template programmatically using the Content API so that your campaign setup is fully reproducible in code.

You can also create templates visually by navigating to Messaging > Content Template Builder in the Twilio Console. If you'd rather design your template through the web, follow along with How to Use Twilio's Content Template Builder for Messaging and then plug the resulting Content SID into the .env file.

In your scripts folder, create a file called CreateTemplate.cs and paste the code below. Notice that .NET 10 single-file apps use #:package directives at the top of the file to resolve NuGet dependencies automatically without a project file, alongside top-level statements for clean execution.

#:package Twilio@8.0.1
#:package DotNetEnv@3.2.0
using DotNetEnv;
using Twilio;
using Twilio.Rest.Content.V1;
// Load environment variables from .env
Env.Load();
string accountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
string authToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
TwilioClient.Init(accountSid, authToken);
var content = await ContentResource.CreateAsync(
    contentCreateRequest: new ContentResource.ContentCreateRequest.Builder()
        .WithFriendlyName("fall-sale-rcs-campaign")
        .WithLanguage("en")
        .WithVariables(new Dictionary<string, string> { { "1", "Alex" } })
        .WithTypes(
            new ContentResource.Types.Builder()
                .WithTwilioCard(
                    new ContentResource.TwilioCard.Builder()
                        .WithTitle("Fall Sale is Here, {{1}}!")
                        .WithSubtitle("Save 25% storewide this weekend")
                        .WithMedia(new List<string> { "https://demo.twilio.com/owl.png" })
                        .WithActions(new List<ContentResource.CardAction> {
                            new ContentResource.CardAction.Builder()
                                .WithType(ContentResource.CardActionType.Url)
                                .WithTitle("Shop the sale")
                                .WithUrl("https://example.com/fall-sale")
                                .Build(),
                            new ContentResource.CardAction.Builder()
                                .WithType(ContentResource.CardActionType.QuickReply)
                                .WithTitle("Send my promo code")
                                .WithId("send-promo-code")
                                .Build(),
                            new ContentResource.CardAction.Builder()
                                .WithType(ContentResource.CardActionType.QuickReply)
                                .WithTitle("Unsubscribe")
                                .WithId("unsubscribe")
                                .Build()
                        })
                        .Build())
                .Build())
        .Build());
Console.WriteLine($"Content Template created at: {content.DateCreated}");
Console.WriteLine($"Content Template created! SID: {content.Sid}");
Console.WriteLine("Save this SID in your .env file as CONTENT_SID.");

This script authenticates with Twilio using your credentials, then creates a Content Template using the TwilioCard type. The WithTitle, and WithSubtitle fields make up the visible copy on the card, Media is the hero image at the top, and each entry in Actions becomes a button underneath. The {{1}} placeholder in the title is a dynamic variable you’ll fill in when you send the campaign, so the greeting is personalized for every recipient.

.NET 10 has recently added the ability to run scripts without including them in your project, but you will need to run this outside of the folder that you save your project in to avoid any confusion between your server and your file-based program. You will also need to duplicate your .env file to this new scripts folder to make sure all the environment variables propagate.

Save the file and run the script from your terminal:

dotnet run CreateTemplate.cs

When this is run correctly, you should see output similar to:

Content Template created! SID: HXaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Save this SID in your .env file as CONTENT_SID.

Copy the Content SID and add it to your .env file:

CONTENT_SID=HXaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Step 3: Build the campaign recipient list

For this tutorial, you’ll store recipients in a simple JSON file. In a production system, this list would come from your CRM, database, or customer data platform.

Create a file called recipients.json in the scripts folder of your project and add the following:

[
    {
        "name": "Dhruv",
        "phoneNumber": "+15551234567"
    },
    {
        "name": "Dylan",
        "phoneNumber": "+15557654321"
    },
    {
        "name": "Matthew",
        "phoneNumber": "+15559876543"
    },
    {
        "name": "Amanda",
        "phoneNumber": "+15559876543"
    }
]

Replace the phone numbers with the E.164-formatted phone numbers of the test devices you registered on your RCS Sender. Any number that isn’t registered as a tester will fail to receive the message.

Step 4: Send the campaign

With your template and recipient list in place, you’re ready to send the campaign. Create a file called SendCampaign.cs in the scripts folder and add the following code:

#:package Twilio@8.0.1
#:package DotNetEnv@3.2.0
#:property JsonSerializerIsReflectionEnabledByDefault=true
using System.Text.Json;
using DotNetEnv;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
// Load environment variables from .env
Env.Load();
string accountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
string authToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
string messagingServiceSid = Environment.GetEnvironmentVariable("MESSAGING_SERVICE_SID");
string contentSid = Environment.GetEnvironmentVariable("CONTENT_SID");
string statusCallbackUrl = Environment.GetEnvironmentVariable("STATUS_CALLBACK_URL") ?? string.Empty;
TwilioClient.Init(accountSid, authToken);
var jsonOptions = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};
var recipients = JsonSerializer.Deserialize<List<Recipient>>(
    File.ReadAllText("./recipients.json"),
    jsonOptions
) ?? new List<Recipient>();
Console.WriteLine($"Starting campaign for {recipients.Count} recipients...");
foreach (var recipient in recipients)
{
    try
    {
        var message = await MessageResource.CreateAsync(
            messagingServiceSid: messagingServiceSid,
            to: new PhoneNumber(recipient.PhoneNumber),
            contentSid: contentSid,
            contentVariables: JsonSerializer.Serialize(new Dictionary<string, string>
            {
                { "1", recipient.Name }
            }),
            statusCallback: !string.IsNullOrEmpty(statusCallbackUrl) ? new Uri(statusCallbackUrl) : null
        );
        Console.WriteLine($"Queued for {recipient.Name} ({recipient.PhoneNumber}) — SID: {message.Sid}");
    }
    catch (Exception error)
    {
        Console.WriteLine($"Failed to queue message for {recipient.PhoneNumber}: {error.Message}");
    }
}
Console.WriteLine("Campaign submitted to Twilio.");
public class Recipient
{
    public string Name { get; set; } = string.Empty;
    public string PhoneNumber { get; set; } = string.Empty;
}

What this code does:

  • The script reads recipients.json into memory and iterates over each entry.
  • messagingServiceSid tells Twilio to send from the Messaging Service you configured, which contains your RCS Sender. Twilio automatically falls back to SMS if a recipient’s device does not support RCS.
  • contentSid references the Content Template you created in Step 1.
  • contentVariables is a JSON string that fills in the {{1}} placeholder with the recipient’s name for personalization.
  • statusCallback is the URL Twilio will hit whenever the message status changes. You’ll build that endpoint in the next step.

Next, you'll need to spin up the webhook server that will receive delivery updates.

Step 5: Track delivery with a status callback webhook

A campaign without measurement is a guess. Twilio can call your server every time a message moves from queued to sent to delivered to read (RCS supports read receipts) so you can build a live picture of how the campaign is performing.

Alter your Program.csfile to contain the following code:

using DotNetEnv;
using Twilio;
using Twilio.TwiML;
Env.Load();
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var stats = new Dictionary<string, int>
{
   { "queued", 0 },
   { "sent", 0 },
   { "delivered", 0 },
   { "read", 0 },
   { "failed", 0 },
   { "undelivered", 0 }
};
app.MapPost("/status", async (HttpRequest req) =>
{
   var form = await req.ReadFormAsync();
   var messageSid = form["MessageSid"].ToString();
   var messageStatus = form["MessageStatus"].ToString();
   var to = form["To"].ToString();
   var errorCode = form["ErrorCode"].ToString();
   if (stats.ContainsKey(messageStatus))
   {
       stats[messageStatus] += 1;
   }
   var errorSuffix = string.IsNullOrEmpty(errorCode) ? "" : $" (error {errorCode})";
   Console.WriteLine($"[{messageStatus}] {messageSid} → {to}{errorSuffix}");
   Console.WriteLine($"Running totals: {{ {string.Join(", ", stats.Select(kv => $"{kv.Key}: {kv.Value}"))} }}");
   return Results.NoContent();
});
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
var url = $"http://127.0.0.1:{port}";
Console.WriteLine($"Webhook server running at {url}/");
app.Run(url);

This code sets up a server that listens for POST requests at /status. Twilio sends status callbacks to this route and will capture message statuses. Of course in a production setting, you should store or send this data off to a database or a CRM for in-depth metric tracking for customers. Message statuses you can expect for RCS include:

  • queued — Twilio has accepted the message.
  • sent — Twilio has handed the message off to the carrier network.
  • delivered — The message reached the recipient’s device.
  • read — The recipient opened the message (RCS-only).
  • failed / undelivered — Something went wrong. The ErrorCode field tells you why.

Start the server in a terminal window:

dotnet run

You should see:

Webhook server running at http://127.0.0.1:3000/

Leave this terminal running.

Step 6: Handle replies from your recipients

Your campaign template includes two suggested reply buttons: Send my promo code and Unsubscribe. When a recipient taps either one, Twilio delivers the button’s payload as an incoming message, which you can respond to with TwiML.

Open Program.cs and add the following route below the /status route:

app.MapPost("/incoming", async (HttpRequest req) =>
{
   var form = await req.ReadFormAsync();
   var from = form["From"].ToString();
   var body = form["Body"].ToString();
   var reply = (body ?? "").Trim();
   Console.WriteLine($"Inbound from {from}: \"{reply}\"");
   var twiml = new MessagingResponse();
   if (reply.ToLower().Contains("send my promo code"))
   {
       twiml.Message("Your promo code is FALL25. It expires Sunday at midnight.");
   }
   else if (reply.ToLower().Contains("unsubscribe"))
   {
       twiml.Message("You have been unsubscribed. Reply START to opt back in.");
       // Process the unsubscription, e.g.
   }
   else
   {
       twiml.Message("Thanks for the message! A team member will get back to you shortly.");
       // Process the incoming message
   }
   return Results.Content(twiml.ToString(), "text/xml");
});

When a user taps a quick reply button, Twilio sends the button’s title as the Body of an inbound message webhook. This handler inspects the body and responds with the appropriate follow-up message using TwiML.

Restart the server so the new route is registered. In the terminal running Program.cs, press Ctrl+C and start it again:

dotnet run

Step 7: Expose your server with ngrok

Twilio needs a public URL to call. Open a new terminal window and run:

ngrok http 3000

ngrok will print a forwarding URL that looks like https://abcd-1234.ngrok-free.app.

 Terminal output from ngrok showing the public forwarding URL pointing to localhost port 3000.
 Terminal output from ngrok showing the public forwarding URL pointing to localhost port 3000.

Copy the https:// forwarding URL and update the STATUS_CALLBACK_URL line in your .env file so it ends with /status:

STATUS_CALLBACK_URL=https://abcd-1234.ngrok-free.app/status

Save the file. Your campaign script will now include this URL as the statusCallback parameter on every message.

Step 8: Wire up the webhooks in the Twilio Console

Now, you need to tell Twilio where to send delivery statuses and inbound messages:

  • In the Twilio Console, navigate to your RCS sender you created: Products & Services > Numbers & Senders > Overview > RCS and open your RCS Sender.
  • Click Configuration from the top.
  • In the Status callback URL field, paste your ngrok URL followed by /status (for example, https://abcd-1234.ngrok-free.app/status).
  • Click Save configuration.
Screenshot of the Twilio Messaging Service Integration page with the Send a webhook option selected and the incoming webhook URL filled in.
Screenshot of the Twilio Messaging Service Integration page with the Send a webhook option selected and the incoming webhook URL filled in.

Next, for incoming messages navigate to messaging services from your Twilio Console: Products & Services > Messaging > Services.

Then, click on your messaging service that is hooked up to your RCS sender. Navigate to the Settings tab.

Twilio settings page displaying options for handling inbound messages for a messaging service.

Under Inbound messages, click the Send a webhook. Scroll further and under Request URL paste your ngrok URL followed by /incoming (for example, https://abcd-1234.ngrok-free.app/incoming). Select HTTP POST for Method.

Screenshot of Twilio messaging service configuration settings page with request URL and method fields.

Scroll down and click Save.

Test the Campaign

You now have the server running, ngrok forwarding traffic, and the webhook wired up. Open a third terminal window (leave your server and ngrok running in their own windows) and send the campaign:

dotnet run SendCampaign.cs

In the send terminal, you should see output like:

Starting campaign for 3 recipients...
Queued for Alex (+15551234567) — SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Queued for Priya (+15557654321) — SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Queued for Sam (+15559876543) — SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Campaign submitted to Twilio.

Switch to the terminal running your server. Within a few seconds, you’ll see status callbacks streaming in as each message moves through the delivery pipeline.

Check your test device. You should see a rich RCS card with your hero image, the personalized greeting, the subtitle, the body, and three buttons.

Smartphone with a screen showing a Twilio Dev message and an image of an owl, offering a sale and options to unsubscribe.

Tap Send my promo code. In the server terminal, you’ll see:

Inbound from +15551234567: "Send my promo code"

And on the phone, you’ll receive the promo code follow-up message. Tap Unsubscribe to see the opt-out response, or send any freeform text to trigger the fallback reply.

Troubleshooting

If something doesn’t behave as expected, work through these common causes:

  • Messages are delivered as SMS instead of RCS. The recipient’s device may not support RCS, or the sender may still be pending approval. Confirm the device is a registered tester on your RCS Sender.
  • failed status with error code 30001 or 63001. Your Messaging Service does not have a sender that can reach the recipient. Verify the RCS Sender is attached to the Messaging Service and the phone number is in E.164 format.
  • Status callbacks never arrive. Confirm that STATUS_CALLBACK_URL in your .env file matches the current ngrok URL (ngrok assigns a new URL every time you restart it) and that the server is running.
  • Inbound webhook not firing. Re-check the webhook URL saved in your Messaging Service’s Integration tab, and make sure the ngrok tunnel is still active.

What’s next?

You’ve built an RCS Business Messaging campaign in .NET: a rich Content Template, a personalized broadcast, live delivery tracking, and interactive reply handling. Here are some ways to take it further:

  • Swap the recipient JSON file for a real audience source like Twilio Segment or your own database.
  • Add a carousel template so a single message can showcase multiple products.
  • Persist status events to a database and build a small dashboard on top of the running totals for real reporting.
  • Secure webhooks with Twilio’s webhook request validation so your /status and /incoming endpoints only accept traffic that originated at Twilio.

For deeper reference material, check out the RCS Business Messaging documentation and the Content API resources.

Amanda Lange is a .NET Engineer of Technical Content. She is here to teach how to create great things using C# and .NET programming. She can be reached at amlange [ at] twilio.com.