4 ways to send SMS with Python

Send SMS from Python with Twilio, Vonage, Plivo, or a notification API. Working code for each option, plus how to add templates and provider failover.

Updated Sep 11, 2026

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

Python has no built-in way to send a text message, so sending SMS means calling a provider's API. That part is a handful of lines with any of the options below. What differs is what you have to build yourself afterward.

This guide covers four options with working code for each: Twilio, Vonage, Plivo, and a notification API layered over any of them.

Which option should you pick?

  • Twilio for the shortest path to a working send, and the largest volume of existing answers when something breaks.
  • Vonage if you are already using Vonage for voice or verification and want one vendor.
  • Plivo if per-message cost is what you are optimizing for at volume.
  • A notification API in front of whichever of those you choose, once SMS is part of a product flow rather than a script. That is the point where templates, opt-outs, retries, and multi-channel routing stop being small problems.

The first three are the same shape: install a package, build a client from credentials, call a send method. Swapping between them is an afternoon. The fourth is a layer rather than a fourth vendor, and you use it with one of the first three.

1. Send SMS with Twilio

Twilio's Programmable Messaging API sends and receives SMS, reports delivery status through webhooks, and schedules sends. It is the most commonly used option in Python, which matters mostly when you are debugging something odd and want prior art to exist.

What you need first

  • A Twilio account, trial or paid.
  • Python 3.8 or newer.
  • An SMS-enabled Twilio number, bought in the Twilio Console.
  • For US consumer traffic, a registered 10DLC campaign or a verified toll-free number.

Install

pip install twilio

Verified against twilio 9.11.1.

Send the message

import os
from twilio.rest import Client
# With no arguments, Client() reads TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN
# from the environment.
client = Client()
message = client.messages.create(
body="Your order has shipped.",
from_=os.environ["TWILIO_PHONE_NUMBER"],
to=os.environ["CELL_PHONE_NUMBER"],
)
print(message.sid, message.status)

Note the trailing underscore in from_, which exists because from is a Python keyword. The status you get back is queued or accepted, not delivered. To learn what actually happened, either poll client.messages(message.sid).fetch() or register a status callback webhook.

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

2. Send SMS with Vonage

Vonage covers SMS, voice, and a Verify API for one-time passcodes. If you already use it for phone verification, sending SMS through the same account avoids a second vendor.

The important thing to know before you copy any older Vonage tutorial: the Python SDK was restructured in v4. The old vonage.Client(key=..., secret=...) followed by vonage.Sms(client) no longer exists. Current code builds an Auth, wraps it in Vonage, and sends a typed SmsMessage.

What you need first

  • A Vonage account, plus your API key and API secret from the Vonage dashboard.
  • Python 3.9 or newer.
  • A Vonage number, or an alphanumeric sender ID where the destination country permits one.

Install

pip install vonage

Verified against vonage 4.8.2, which pulls in vonage-sms 1.2.0 for the SMS models.

Send the message

import os
from vonage import Auth, Vonage
from vonage_sms import SmsMessage, SmsResponse
client = Vonage(
Auth(
api_key=os.environ["VONAGE_API_KEY"],
api_secret=os.environ["VONAGE_API_SECRET"],
)
)
message = SmsMessage(
to=os.environ["CELL_PHONE_NUMBER"],
from_="AcmeInc",
text="Your order has shipped.",
)
response: SmsResponse = client.sms.send(message)
print(response.model_dump(exclude_unset=True))

SmsMessage is a Pydantic model, so a malformed number or a missing field raises a validation error before any HTTP request goes out. That is a real improvement over the dict-based older API, and it is also why old snippets fail loudly rather than silently.

Where it gets thin: alphanumeric sender IDs are not accepted in every country, including the US, so you will still need a number for some destinations. And as with Twilio, everything above delivery is yours to build.

3. Send SMS with Plivo

Plivo does the same job as Twilio at generally lower per-message pricing, which is why it turns up in high-volume sending. The tradeoff is a smaller community, so unusual failures take longer to diagnose.

What you need first

  • A Plivo account, plus your Auth ID and Auth Token.
  • Python 3.7 or newer.
  • A Plivo number or registered sender ID. On a trial account you can only send to numbers you have verified with Plivo, which is the usual reason a first send silently goes nowhere.

Install

pip install plivo

Verified against plivo 4.62.0.

Send the message

import os
import plivo
# With no arguments, RestClient() reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN
# from the environment.
client = plivo.RestClient()
response = client.messages.create(
src=os.environ["PLIVO_PHONE_NUMBER"],
dst=os.environ["CELL_PHONE_NUMBER"],
text="Your order has shipped.",
)
print(response["message_uuid"])

Both src and dst need E.164 formatting. For delivery tracking, stand up an endpoint and give Plivo its URL, and Plivo will post each status transition as the message moves.

Where it gets thin: the same gaps as Twilio and Vonage, since it is the same layer.

4. Send SMS with a notification API

The three options above solve delivery. Here is what usually shows up in the fortnight after delivery works:

  • Message copy is a string literal in your code, so a wording change is a deploy.
  • Someone replies STOP and you now own an opt-out list and the legal 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, Vonage, or Plivo doing the delivery, and your Python code 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.
  • 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 Plivo account from the sections above. You are not changing who delivers the SMS.

Install

pip install trycourier

Verified against trycourier 9.5.0, which requires Python 3.9 or newer.

Send the message

from courier import Courier
# Reads COURIER_API_KEY from the environment.
client = Courier()
response = client.send.message(
message={
"to": {"phone_number": "+15558675309"},
"content": {
"title": "Order update",
"body": "Hi Erika, your order has shipped.",
},
"routing": {"method": "single", "channels": ["sms"]},
}
)
print(response.request_id)

Note the import: the package is trycourier on PyPI but the module is courier. That mismatch trips people up on the first line.

The version you will run in production references a template you published in Courier and addresses the user rather than a raw number:

response = client.send.message(
message={
"to": {"user_id": "user-123"},
"template": "ORDER_SHIPPED",
"data": {"name": "Erika", "tracking_url": "https://example.com/t/abc"},
}
)

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

If you need an async client, import AsyncCourier instead and await the same call.

Where it gets thin: one more service in the path and one more concept before your first send. It is also 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 Python that compiles and Python 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 four options

TwilioVonagePlivoNotification API
Packagetwilio 9.11.1vonage 4.8.2plivo 4.62.0trycourier 9.5.0
Importfrom twilio.rest import Clientfrom vonage import Auth, Vonageimport plivofrom courier import Courier
Send callclient.messages.create()client.sms.send(SmsMessage(...))client.messages.create()client.send.message()
Typed request modelsnoyes, Pydanticnoyes
Async clientnononoyes, AsyncCourier
Other channelsSMS, WhatsApp, voice, emailSMS, voice, verificationSMS, WhatsApp, voicewhatever you connect
Templates outside your codenononoyes
Preferences and opt-outsyou build ityou build ityou build itbuilt in
Provider failovern/an/an/ayes, across providers
Best forfirst send, widest communityexisting Vonage usershigh volume, cost-sensitiveSMS inside a product flow

If you need one text message sent this afternoon, pip install twilio and stop reading. If SMS is becoming a feature, the decision you will wish you had made earlier is the layer that lets you change provider, wording, and channel without touching Python.

Next steps


FAQ

Frequently asked questions

Install a provider SDK with pip, put your credentials in environment variables, and call that SDK's send method with a recipient number and a message body. With Twilio that is pip install twilio, then Client().messages.create(body=..., from_=..., to=...). Python has no built-in SMS capability, so every option means calling some provider's HTTP API, directly or through a wrapper.

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.