Skip to main contentSkip to navigation
Back to Articles
Tutorial
5min read

How to Send SMS via API in Python (2026 Guide)

SE

smsroute editorial

June 8, 2026

If you can run a Python script, you can send an SMS. The whole thing is one POST request. This guide shows the working version first, then peels back every line so you understand what you are actually sending, what comes back, and what to do when it doesn't.

The 30-second version

Install the only dependency you'll need, set two environment variables, and run this:

import os
import requests

response = requests.post(
    "https://api.smsroute.cc/v1/send",
    headers={
        "Authorization": f"Bearer {os.environ['SMSROUTE_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "to": "+15555550100",
        "from": "MyApp",
        "message": "Your verification code is 481902.",
    },
    timeout=10,
)
response.raise_for_status()
print(response.json())

That is the whole loop. The rest of this guide explains each piece, the response shape, and the two error patterns that will absolutely hit you in production.

What the API is actually doing

You POST a JSON body with three things: the destination number, the sender ID (what shows up on the recipient's phone), and the text. The server authenticates you via the Authorization: Bearer header, queues the message into the carrier network, and returns a message_id synchronously. The delivery itself is asynchronous — the API has accepted the message; it hasn't reached the phone yet.

That distinction matters. A 200 response means "we have committed to delivering this." It does not mean "the phone received it." If you need the second guarantee, you read the next section.

Handling the delivery receipt

For anything important — OTPs, transaction alerts, appointment reminders — you need a webhook. The flow is: you give the API a URL on your server, and the gateway POSTs to it the moment a delivery receipt comes back from the carrier. Typical end-to-end latency is 800ms to 2s from your API call to the webhook firing.

from flask import Flask, request

app = Flask(__name__)

@app.post("/sms/delivered")
def delivered():
    payload = request.get_json(force=True)
    # payload looks like:
    # {"message_id": "msg_abc123",
    #  "status": "delivered" | "failed" | "undelivered",
    #  "delivered_at": "2026-06-08T14:23:01Z",
    #  "error_code": null | 0 | 27}
    print(payload)
    return "", 204

If you skip webhooks, the only signal you have is your own DB row, marked "sent" the moment the API call returned. You will not know if the carrier silently dropped it. That is the "silent failure" the CTO on the homepage is worried about.

The two errors that hit everyone

1. The number format

You must send the number in E.164 format: a leading +, the country code, then the national number with no spaces, no dashes, no parentheses. +15555550100 is valid; 555-555-0100, (555) 555-0100, and 0015555550100 all fail. This is the single most common cause of "the message never arrived."

2. The 429

Hit the API too fast and you will get a 429 with a Retry-After header in seconds. Respect it. A simple backoff:

import time
import requests
from requests.exceptions import HTTPError

def send_with_backoff(payload, attempts=4):
    for i in range(attempts):
        r = requests.post(url, json=payload, headers=headers, timeout=10)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", 2 ** i))
            time.sleep(wait)
            continue
        r.raise_for_status()
        return r.json()
    raise HTTPError("rate limited after retries")

Sending in bulk

Do not loop. The bulk endpoint accepts an array of recipients in a single request and is rate-limited differently than the single-send path:

requests.post(
    "https://api.smsroute.cc/v1/send/bulk",
    headers=headers,
    json={
        "from": "MyApp",
        "message": "Flash sale: 30% off for the next 4 hours.",
        "recipients": ["+15555550100", "+15555550101", "+447700900123"],
    },
    timeout=30,
)

Verifying the number before you bill it

About 15-20% of phone numbers in any given list are dead, ported away, or simply invalid. Hit the HLR endpoint first to know whether the number is alive and on-network. One HLR lookup costs a fraction of one SMS, and an SMS to a dead number costs the full amount and burns deliverability reputation.

requests.get(
    "https://api.smsroute.cc/v1/hlr/+15555550100",
    headers=headers,
    timeout=10,
).json()

What good looks like in production

Four rules, in order of importance:

  1. Use webhooks. A synchronous "sent" response is a queue acknowledgment, not a delivery confirmation.
  2. Numbers in E.164 only. Validate at the edge of your system, reject anything else, do not try to fix it downstream.
  3. Idempotency key on every send. A unique Idempotency-Key header (your order ID, your user ID + a counter) prevents double-sends if your client retries a timeout.
  4. Send OTP messages through a separate sender ID and route. Mix them with marketing traffic and carriers throttle you during peak.

Where to go from here

Once the single-send path is solid, the next things that matter in production are: 10DLC registration if you are sending to US numbers at volume, DLT registration for India, and a fallback provider wired in for failover. Each of those is its own decision. The send loop itself is the simple part.

Run this against a real gateway

Sign up with smsroute, get an API key, and the snippet at the top of this article will work unchanged. No KYC, no card, your first credits are on us.

Get an API Key
Keywords:python sms apisend sms pythonpython requests smssms api tutorialsms gateway python

Related Articles