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.
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.
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
Install
pip install twilio
Verified against twilio 9.11.1.
Send the message
import osfrom 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.
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
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 osfrom vonage import Auth, Vonagefrom vonage_sms import SmsMessage, SmsResponseclient = 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.
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
Install
pip install plivo
Verified against plivo 4.62.0.
Send the message
import osimport 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.
The three options above solve delivery. Here is what usually shows up in the fortnight after delivery works:
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:
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.
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.
| Twilio | Vonage | Plivo | Notification API | |
|---|---|---|---|---|
| Package | twilio 9.11.1 | vonage 4.8.2 | plivo 4.62.0 | trycourier 9.5.0 |
| Import | from twilio.rest import Client | from vonage import Auth, Vonage | import plivo | from courier import Courier |
| Send call | client.messages.create() | client.sms.send(SmsMessage(...)) | client.messages.create() | client.send.message() |
| Typed request models | no | yes, Pydantic | no | yes |
| Async client | no | no | no | yes, AsyncCourier |
| Other channels | SMS, WhatsApp, voice, email | SMS, voice, verification | SMS, WhatsApp, voice | whatever you connect |
| Templates outside your code | no | no | no | yes |
| Preferences and opt-outs | you build it | you build it | you build it | built in |
| Provider failover | n/a | n/a | n/a | yes, across providers |
| Best for | first send, widest community | existing Vonage users | high volume, cost-sensitive | SMS 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.
FAQ
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.twilio if you want the largest community and the shortest path to a first send, vonage if you are already on Vonage, and plivo if per-message cost is your deciding factor. All three are single-provider SDKs. If SMS is one channel in a product flow rather than a standalone script, put a notification API such as trycourier in front of whichever one you pick so templates, preferences, and failover are not your code.vonage.Client no longer exists. The Vonage Python SDK v4 restructured the whole client: you now build Vonage(Auth(api_key=..., api_secret=...)) and send with a typed SmsMessage from the vonage_sms package, instead of vonage.Client(key=..., secret=...) followed by vonage.Sms(client).send_message({...}). Any tutorial showing the old two-step shape predates v4.client.send.message() call can deliver SMS, email, push, or in-app, because you address a user_id and the platform resolves the channel from routing rules and that user's preferences. Adding a channel becomes configuration rather than new code.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.