3 ways to send SMS with Rust

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.

Which option should you pick?

  • The community 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.
  • Infobip if you want a provider that publishes its own Rust SDK, so the client is maintained by the same people as the API.
  • A notification API over plain reqwest once SMS is part of a product flow. This is also the most future-proof of the three in Rust specifically, because a hand-written HTTP call has no crate to go unmaintained.

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.

1. Send SMS with the Twilio crate

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

  • A Twilio account, plus your Account SID and Auth Token.
  • Rust 1.70 or newer.
  • An SMS-enabled Twilio number. On a trial you can only send to numbers you have verified.
  • For US consumer traffic, a registered 10DLC campaign or a verified toll-free number.

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.

2. Send SMS with Infobip

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

  • An Infobip account, plus your API key and your account's base URL. Infobip gives each account its own hostname, so the base URL is per-account rather than global.
  • Rust 1.70 or newer.
  • A registered sender ID or number.

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.

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 a compiled binary, so a wording change means a rebuild and redeploy.
  • 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 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:

  • 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.
  • 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 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.

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 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.

Comparing the three options

Twilio crateInfobip SDKNotification API
Cratetwilio 1.1.0infobip_sdk 0.6.1none, reqwest 0.13.5
Official?no, communityyes, vendor-publishedno SDK, REST API is official
Last releasedMarch 2024April 2024n/a
Send callclient.send_message(...)sms_client.send(...)POST /send
API coveragesubsetSMS, email, voice, WhatsAppfull REST API
Templates outside your binarynostored templatesyes
Preferences and opt-outsyou build ityou build itbuilt in
Provider failovern/an/ayes, across providers
Risk if unmaintainedmigrate to another clientmigrate to another clientnone, it is your own HTTP code
Best forfirst send with Twilioone vendor, official clientSMS 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.

Next steps


FAQ

Frequently asked questions

Add a crate for your provider, build a client from credentials in environment variables, and await its send method. With the community 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.

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.