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.
Twilio's Programmable Messaging API sends and receives SMS and MMS, reports delivery status through webhooks, and schedules sends.
What you need first
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.
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
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.", // bodyrecipients);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.
Both options above solve delivery. Here is what usually turns up in the fortnight afterward:
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:
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.
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.
| Twilio | MessageBird | Notification API | |
|---|---|---|---|
| NuGet package | Twilio 8.0.1 | MessageBird 4.0.1 | TryCourier 7.5.0 |
| Client setup | TwilioClient.Init(sid, token) | Client.CreateDefault(key) | new CourierClient() |
| Send call | MessageResource.CreateAsync(...) | client.SendMessage(...) | await client.Send.Message(...) |
| Async support | yes | no, synchronous only | yes |
| Reads credentials from env | you pass them | you pass them | yes |
| Recipient type | PhoneNumber | long[] | UserRecipient |
| Other channels | SMS, WhatsApp, voice, email | SMS, WhatsApp, voice | whatever you connect |
| Templates outside your code | no | no | yes |
| Preferences and opt-outs | you build it | you build it | built in |
| Provider failover | n/a | n/a | yes, across providers |
| Best for | first send, widest community | per-country pricing | SMS 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.
FAQ
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.Twilio at 8.0.1 for Twilio, or MessageBird at 4.0.1 for MessageBird. If you want a notification layer over either one, TryCourier at 7.5.0. Note the Courier package name: an older Courier.Client package exists on NuGet but stalled at 0.2.0 in November 2024, so TryCourier is the maintained one.dotnet add package TryCourier, then CourierClient client = new() reads COURIER_API_KEY from the environment and await client.Send.Message(parameters) sends. You do not need to hand-roll HttpClient calls against the REST API, which is what older tutorials show. The package targets .NET Standard 2.0 or later.Environment.GetEnvironmentVariable or bind them from configuration and a secrets store, rather than assigning string literals. The Courier SDK does it for you: new CourierClient() with no arguments picks up COURIER_API_KEY. Keys committed to a repository get scraped and used quickly, and a key in published documentation is worse, because it stays there.UserRecipient.UserID and reference a template, and the platform resolves whether that becomes SMS, email, push, or in-app based on routing and the user's stored preferences. Adding a channel later is a configuration change rather than a second integration.Keep exploring
One API, every channel
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.