Send SMS from Rust with the Twilio crate, the Infobip SDK, or a notification API over reqwest. Working code for each, plus which crates you can actually trust.
Updated Sep 11, 2026
Last updated: September 2026. Package versions verified against crates.io on 2026-09-11.
Rust has no built-in way to send a text message, and unlike most languages it also has no official SDK from the major SMS providers. So sending SMS from Rust means picking between a community crate, a provider that does ship a Rust SDK, and calling a REST API yourself with reqwest.
All three are short. The tradeoff is between convenience now and how much you trust the crate to still be there in two years.
twilio crate for the shortest path to a working send against the most widely used provider. It is not official, and its last release was March 2024.A note on all three: every option is async, so you need a runtime. Tokio is the default choice because reqwest is built on it. If a snippet uses #[tokio::main] and will not compile, check that Tokio is actually in your Cargo.toml with the macros feature before looking anywhere else.
Twilio does not publish an official Rust helper library. The community twilio crate is the most-used substitute, with around 255,000 downloads.
Be clear-eyed about what that means. It was last released in March 2024, it covers a subset of the API, and if it stops being maintained you are migrating. For sending a text message, that subset is enough.
What you need first
Add the dependencies
[dependencies]twilio = "1.1.0"tokio = { version = "1", features = ["macros", "rt-multi-thread"] }dotenvy = "0.15"
Verified against twilio 1.1.0. Note the file is Cargo.toml with a capital C, which matters on case-sensitive filesystems.
Send the message
use std::env;use twilio::{Client, OutboundMessage};#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {dotenvy::dotenv().ok();let client = Client::new(&env::var("TWILIO_ACCOUNT_SID")?,&env::var("TWILIO_AUTH_TOKEN")?,);let from = env::var("TWILIO_PHONE_NUMBER")?;let to = env::var("CELL_PHONE_NUMBER")?;let message = client.send_message(OutboundMessage::new(&from, &to, "Your order has shipped.")).await?;println!("{:?}", message.sid);Ok(())}
Run it with cargo run.
Returning Result from main and using ? is worth doing here rather than .unwrap() everywhere: a missing environment variable then prints which variable was missing instead of panicking with a bare None.
If you need API surface the crate does not cover, generate a client from Twilio's OpenAPI spec:
openapi-generator generate -g rust \-i https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json \-o twilio-rust
That gives you complete coverage at the cost of ergonomics: the generated create_message takes its optional parameters positionally, so a send becomes one real argument followed by a long run of None. It also means regenerating whenever the spec changes. Reach for it when the crate genuinely lacks what you need, not by default.
Where it gets thin: an unofficial client, one provider, message copy in your source, and delivery events arriving as raw webhooks you handle yourself.
Infobip publishes its own Rust SDK, which is unusual among SMS providers and is the main reason to consider it here: the client is maintained by the same people as the API. Infobip also covers email, voice, WhatsApp, and Messenger.
What you need first
Add the dependencies
[dependencies]infobip_sdk = "0.6.1"tokio = { version = "1", features = ["macros", "rt-multi-thread"] }reqwest = "0.13"serde_json = "1"
Verified against infobip_sdk 0.6.1. The crate was last published in April 2024, so it is maintained but not fast-moving. The last three dependencies are the ones most tutorials omit while using them in the code.
Send the message
use infobip_sdk::api::sms::SmsClient;use infobip_sdk::configuration::Configuration;use infobip_sdk::model::sms::{Destination, Message, SendRequestBody};#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {// Reads IB_API_KEY and IB_BASE_URL from the environment.let sms_client = SmsClient::with_configuration(Configuration::from_env_api_key()?);let mut message = Message::new(vec![Destination::new("+15558675309".to_string())]);message.text = Some("Your order has shipped.".to_string());let response = sms_client.send(SendRequestBody::new(vec![message])).await?;println!("{}", serde_json::to_string(&response.body)?);Ok(())}
Configuration::from_env_api_key() reads both the key and the base URL from the environment, which is why there is no hostname in the code. Message::new takes the destinations and the text is set afterward, because a message can carry several destinations and optional fields.
Where it gets thin: same layer as Twilio, so the same gaps above delivery. The SDK is at 0.x, so treat its API as subject to change.
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 Infobip delivering, and your Rust code stops knowing which one it is.
There is no Courier SDK for Rust, so this is a plain HTTP call. That turns out to be an advantage rather than a compromise: there is no third-party crate in the path to go unmaintained, and the request body is identical to what the official SDKs send in other languages.
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 Infobip account from the sections above; you are not changing who delivers.
Add the dependencies
[dependencies]reqwest = { version = "0.13", features = ["json"] }serde_json = "1"tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Verified against reqwest 0.13.5. No Courier crate is needed.
Send the message
use std::env;use serde_json::json;#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {let api_key = env::var("COURIER_API_KEY")?;let body = json!({"message": {"to": { "phone_number": "+15558675309" },"content": {"title": "Order update","body": "Hi Erika, your order has shipped."},"routing": { "method": "single", "channels": ["sms"] }}});let response = reqwest::Client::new().post("https://api.courier.com/send").bearer_auth(&api_key).json(&body).send().await?.error_for_status()?;let result: serde_json::Value = response.json().await?;println!("{}", result["requestId"]);Ok(())}
error_for_status() is the line that matters. Without it, reqwest treats a 401 or a 422 as a successful request, and you print a requestId of null while nothing was sent.
The version you will run in production references a template you published in Courier and addresses the user rather than a raw number:
let body = json!({"message": {"to": { "user_id": "user-123" },"template": "ORDER_SHIPPED","data": { "name": "Erika", "tracking_url": "https://example.com/t/abc" }}});
Nothing in that body 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.
If you would rather have typed request bodies than json!, define serde::Serialize structs for the message shape. For a handful of notification types that is worth doing; for one send it is not.
Where it gets thin: one more service in the path, no typed client to catch payload mistakes at compile time, and it is the wrong tool for two-way SMS conversations.
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 Rust that compiles and Rust 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 crate | Infobip SDK | Notification API | |
|---|---|---|---|
| Crate | twilio 1.1.0 | infobip_sdk 0.6.1 | none, reqwest 0.13.5 |
| Official? | no, community | yes, vendor-published | no SDK, REST API is official |
| Last released | March 2024 | April 2024 | n/a |
| Send call | client.send_message(...) | sms_client.send(...) | POST /send |
| API coverage | subset | SMS, email, voice, WhatsApp | full REST API |
| Templates outside your binary | no | stored templates | yes |
| Preferences and opt-outs | you build it | you build it | built in |
| Provider failover | n/a | n/a | yes, across providers |
| Risk if unmaintained | migrate to another client | migrate to another client | none, it is your own HTTP code |
| Best for | first send with Twilio | one vendor, official client | SMS inside a product flow |
Rust changes the usual calculation a little. In most languages the SDK is the safe default and raw HTTP is the fallback. Here the official SDKs mostly do not exist, so a hand-written reqwest call against a documented REST API is often the most durable option, not the least.
FAQ
twilio crate that is twilio::Client::new(sid, token) then client.send_message(OutboundMessage::new(from, to, body)).await. Rust has no built-in SMS capability, and every provider call is async, so you need a runtime such as Tokio.twilio crate, at 1.1.0 with roughly 255,000 downloads, last released in March 2024. Your alternatives are generating a client from Twilio's OpenAPI spec, or calling the REST API directly with reqwest.message object. That is about fifteen lines with reqwest and serde_json, and the request shape is identical to what the SDKs send.twilio_config.basic_auth = Some(...) without the preceding let mut twilio_config = Configuration::default();. Rust catches this at compile time rather than at runtime, so add the declaration and make it mutable before assigning to its fields.tokio with the macros and rt-multi-thread features, then annotate main with #[tokio::main]. A common cause of confusing compile errors is copying a snippet that uses #[tokio::main] without adding Tokio to Cargo.toml, since the attribute macro will not resolve.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.