3 ways to send SMS with C#

Send SMS from C# with Twilio, MessageBird, or a notification API. Current NuGet packages and async-friendly code, plus how to choose between the three.

Updated Sep 11, 2026

Last updated: September 2026. Package versions verified against NuGet on 2026-09-11.

.NET has no built-in way to send a text message, so every option here calls a third-party API. The send itself is short. What differs is how much of the surrounding machinery you end up owning.

This guide covers three options with working code: Twilio, MessageBird, and a notification API layered over either.

Which option should you pick?

  • Twilio for the shortest path to a working send and the largest body of existing answers when something breaks.
  • MessageBird if you want per-message pricing that varies by destination and are comfortable with a .NET SDK that sees infrequent releases.
  • A notification API in front of either one, once SMS is part of a product flow rather than a one-off. That is where templates, per-user opt-outs, retries, and multi-channel routing stop being small problems.

1. Send SMS with Twilio

Twilio's Programmable Messaging API sends and receives SMS and MMS, reports delivery status through webhooks, and schedules sends.

What you need first

  • A Twilio account, trial or paid.
  • .NET 6 or newer.
  • An SMS-enabled Twilio number.
  • On a trial account, the destination number added to your verified caller IDs, plus geographic permissions enabled for that country in the Console under Messaging settings. Both are easy to miss and both cause a silent non-delivery.
  • For US consumer traffic, a registered 10DLC campaign or a verified toll-free number.

Install

dotnet add package Twilio

Verified against Twilio 8.0.1. Do not pin 5.x from an older tutorial.

Send the message

using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
TwilioClient.Init(
Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID"),
Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN")
);
var message = await MessageResource.CreateAsync(
body: "Your order has shipped.",
from: new PhoneNumber(Environment.GetEnvironmentVariable("TWILIO_PHONE_NUMBER")),
to: new PhoneNumber(Environment.GetEnvironmentVariable("CELL_PHONE_NUMBER"))
);
Console.WriteLine($"{message.Sid} {message.Status}");

Prefer CreateAsync over Create in anything that handles requests; the synchronous version blocks a thread on network I/O.

The Status you get back is queued or accepted, not delivered. To learn what actually happened, either fetch the message again by SID or register a status callback webhook.

Where it gets thin: one provider, message copy in your source, and delivery events arriving as raw webhooks you store and interpret yourself.

2. Send SMS with MessageBird

MessageBird bills per message with rates that vary by destination country.

One caveat before you commit: the .NET SDK is at 4.0.1 and its repository was last updated in December 2023. It works, but it is not on the same release cadence as Twilio's. MessageBird now trades as Bird, and its newer platform SDK is TypeScript-only, so .NET users stay on this library.

What you need first

  • A MessageBird account with a verified phone number, which unlocks free test credit.
  • Both keys from the dashboard: a test key that validates requests without sending, and a live key.
  • .NET 6 or newer.
  • A registered sender ID or number.

Install

dotnet add package MessageBird

Verified against MessageBird 4.0.1.

Send the message

using MessageBird;
using MessageBird.Objects;
Client client = Client.CreateDefault(
Environment.GetEnvironmentVariable("MESSAGEBIRD_API_KEY")
);
// SendMessage takes recipients as long[], so the leading plus sign
// is dropped here. Keep numbers in E.164 form everywhere else.
long[] recipients = { 15558675309 };
try
{
Message message = client.SendMessage(
"AcmeInc", // originator: your sender ID
"Your order has shipped.", // body
recipients
);
Console.WriteLine(message.Id);
}
catch (Exception e)
{
Console.Error.WriteLine($"Send failed: {e.Message}");
}

Argument order is originator, body, then recipients. The originator is the sender identity the recipient sees, so it takes a sender ID or number, not the message text.

The long[] recipients parameter is a quirk of this SDK: it means you cannot pass a +-prefixed string, and numbers with a leading zero will not survive the conversion. Store numbers as E.164 strings in your own code and convert at the call site.

Where it gets thin: same layer as Twilio, so the same gaps, plus a less active .NET SDK.

3. Send SMS with a notification API

Both options above solve delivery. Here is what usually turns up in the fortnight afterward:

  • Message copy is a string literal in compiled code, so a wording change is a deploy.
  • Someone replies STOP and you now own an opt-out list and the obligation behind it.
  • The same event needs to reach one user by SMS and another by email.
  • Your provider has a bad hour and there is no second path.
  • Twenty events fire in a minute and one user gets twenty texts.
  • Someone asks how many notifications failed last week and you cannot answer.

A notification API such as Courier sits above your SMS provider and handles those. You keep Twilio or MessageBird delivering, and your C# stops knowing which one it is.

What the extra layer gives you:

  • Automatic failover between providers when one starts erroring.
  • Journeys for multi-step sequences, with digest and throttling nodes so a burst of events is not a burst of texts.
  • One send endpoint for SMS, email, push, in-app, Slack, and Microsoft Teams.
  • Per-user, per-channel preferences enforced before a send.
  • Templates edited outside your codebase, with localization per user. For a compiled language this takes copy changes off your release cycle.
  • One log covering every channel.

Set it up

Create a free account: the Developer plan includes 10,000 messages a month, and past that it's $0.005 per message, the same rate on every channel. Then connect an SMS provider under Channels using the credentials that provider already gave you. This is the same Twilio or MessageBird account from the sections above; you are not changing who delivers.

Install

dotnet add package TryCourier

Verified against TryCourier 7.5.0, targeting .NET Standard 2.0 or later. Note the package name: an older Courier.Client package exists on NuGet but stalled at 0.2.0 in November 2024. TryCourier is the maintained one.

Send the message

using TryCourier;
using TryCourier.Models;
using TryCourier.Models.Send;
// Reads COURIER_API_KEY from the environment.
CourierClient client = new();
SendMessageParams parameters = new()
{
Message = new()
{
To = new UserRecipient { PhoneNumber = "+15558675309" },
Template = "ORDER_SHIPPED",
},
};
var response = await client.Send.Message(parameters);
Console.WriteLine(response);

The version you will run in production addresses the user rather than a raw number, so the platform can apply their channel preferences:

SendMessageParams parameters = new()
{
Message = new()
{
To = new UserRecipient { UserID = "user-123" },
Template = "ORDER_SHIPPED",
Data = new Dictionary<string, JsonElement>
{
{ "name", JsonSerializer.SerializeToElement("Erika") },
},
},
};

Nothing in that call names SMS. Routing and the user's stored preferences decide the channel, so moving a notification to push, or sending both, is configuration rather than a rebuild.

Data takes Dictionary<string, JsonElement>, so template variables go through JsonSerializer.SerializeToElement. That is more ceremony than the dynamic-typed SDKs need, and it is why the second example imports System.Text.Json.

If you prefer configuration over environment variables, new CourierClient { ApiKey = "..." } works too, but read that key from configuration rather than writing it into source.

Where it gets thin: one more service in the path, and it is the wrong tool for two-way SMS conversations, which belong with your provider's inbound APIs.

Building this with an AI agent

If you are writing this in Cursor, Claude Code, or Codex, connect the agent to Courier directly instead of pasting snippets. It then works from the current SDK shapes rather than the 2022-era API that most training data still contains, which is the difference between C# that compiles and C# that does not.

The MCP server gives the agent typed tool calls. In Claude Code:

claude mcp add --transport http courier https://mcp.courier.com --header api_key:YOUR_API_KEY

Courier Skills adds the practices that are easy to get wrong, such as never batching a one-time passcode:

npx skills add trycourier/courier-skills

Both sit alongside the Courier CLI for scripting and CI. courier.com/agents collects the setup for Cursor, Claude Code, Codex, and VS Code in one place, and Build with AI has the machine-readable docs index an agent can fetch on its own.

Then ask in plain language: "send an SMS when an order ships, and fall back to email if it fails." The agent picks the right primitive, and knows which channels need a recorded opt-in.

This matters for the same reason this guide version-stamps every snippet. An agent working from stale training data will confidently write a send call that no longer exists.

Comparing the three options

TwilioMessageBirdNotification API
NuGet packageTwilio 8.0.1MessageBird 4.0.1TryCourier 7.5.0
Client setupTwilioClient.Init(sid, token)Client.CreateDefault(key)new CourierClient()
Send callMessageResource.CreateAsync(...)client.SendMessage(...)await client.Send.Message(...)
Async supportyesno, synchronous onlyyes
Reads credentials from envyou pass themyou pass themyes
Recipient typePhoneNumberlong[]UserRecipient
Other channelsSMS, WhatsApp, voice, emailSMS, WhatsApp, voicewhatever you connect
Templates outside your codenonoyes
Preferences and opt-outsyou build ityou build itbuilt in
Provider failovern/an/ayes, across providers
Best forfirst send, widest communityper-country pricingSMS inside a product flow

If you need one text sent this afternoon, add the Twilio package and stop reading. If SMS is becoming a feature of a .NET application, the notification layer is worth the extra hop, mostly because it gets message copy out of compiled code so wording changes stop requiring a release.

Next steps


FAQ

Frequently asked questions

Add a provider's NuGet package, initialize its client with credentials from environment variables, and call its send method with a sender number, a recipient number in E.164 format, and a body. With Twilio that is TwilioClient.Init(sid, token) followed by MessageResource.Create(body:, from:, to:). .NET has no built-in SMS capability, so every option calls a provider's HTTP API.

One API, every channel

Ship notifications without the boilerplate

Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.

Last updated Sep 11, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.