Back to Blog
Developer Guides

How to Send SMS with Python: A Step-by-Step Tutorial (2026)

Collins VidzroCollins Vidzro2026-08-109 min read
How to Send SMS with Python: A Step-by-Step Tutorial (2026)

How to Send SMS with Python: A Step-by-Step Tutorial (2026)

This tutorial covers sending SMS from Python using nothing but the standard requests library against a plain REST API — no heavyweight SDK required. By the end you'll have a working Flask endpoint you can adapt into any real project.


1. Prerequisites

You'll need:

  • Python 3.9+
  • A Sendexa account — sign up at sendexa.co for a sandbox API key and secret
  • The requests library

Install requests (and python-dotenv for environment variable loading):

pip install requests python-dotenv

2. Set Up Your API Credentials

Create a .env file in your project root — and make sure it's in .gitignore before you commit anything:

SENDEXA_API_KEY=your_api_key_here
SENDEXA_API_SECRET=your_api_secret_here

3. Send Your First SMS

Sendexa's API uses HTTP Basic authentication, which requests supports natively via the auth parameter — no manual header encoding needed:

# send_test.py
import os
import requests
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.environ["SENDEXA_API_KEY"]
API_SECRET = os.environ["SENDEXA_API_SECRET"]

def send_sms(to: str, message: str, sender: str = "SENDEXA") -> dict:
    response = requests.post(
        "https://api.sendexa.co/v1/sms/send",
        auth=(API_KEY, API_SECRET),
        json={"to": to, "from": sender, "message": message},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    result = send_sms("+233501234567", "Hello from Python! 🚀")
    print(result)

Run it:

python send_test.py

A successful call returns a JSON response confirming the message was accepted:

{
  "success": true,
  "code": "SMS_SENT",
  "message": "Message accepted for delivery.",
  "data": {
    "messageId": "MSG-8f21c4a9",
    "delivery": {
      "status": "PENDING",
      "route": { "type": "domestic", "destination": "Ghana" }
    },
    "billing": { "units": 1, "totalCost": 0.01, "currency": "USD" }
  }
}

4. Build a Flask Endpoint

Wrap the send logic in an actual API route your frontend or other services can call:

# app.py
import os
import re
import requests
from flask import Flask, request, jsonify
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)

API_KEY = os.environ["SENDEXA_API_KEY"]
API_SECRET = os.environ["SENDEXA_API_SECRET"]
E164_PATTERN = re.compile(r"^\+[1-9]\d{7,14}$")


def is_valid_e164(phone_number: str) -> bool:
    return bool(E164_PATTERN.match(phone_number))


@app.route("/api/send-sms", methods=["POST"])
def send_sms_endpoint():
    data = request.get_json(silent=True) or {}
    to = data.get("to")
    message = data.get("message")

    if not to or not message:
        return jsonify({"error": "'to' and 'message' are required"}), 400

    if not is_valid_e164(to):
        return jsonify({"error": "Phone number must be in E.164 format, e.g. +233501234567"}), 400

    try:
        response = requests.post(
            "https://api.sendexa.co/v1/sms/send",
            auth=(API_KEY, API_SECRET),
            json={"to": to, "from": "SENDEXA", "message": message},
            timeout=10,
        )
        response.raise_for_status()
        result = response.json()
        return jsonify({"success": True, "messageId": result["data"]["messageId"]}), 200

    except requests.exceptions.HTTPError as error:
        status = error.response.status_code if error.response is not None else 502
        app.logger.error("SMS send failed: %s", error)
        return jsonify({"success": False, "error": "Failed to send SMS"}), status
    except requests.exceptions.RequestException as error:
        app.logger.error("SMS send failed: %s", error)
        return jsonify({"success": False, "error": "Failed to send SMS"}), 502


if __name__ == "__main__":
    app.run(port=3000)

5. Handling Errors Properly

Different failure modes deserve different handling rather than one generic except:

try:
    response = requests.post(
        "https://api.sendexa.co/v1/sms/send",
        auth=(API_KEY, API_SECRET),
        json={"to": to, "from": "SENDEXA", "message": message},
        timeout=10,
    )
    response.raise_for_status()
except requests.exceptions.HTTPError as error:
    status = error.response.status_code
    if status == 401:
        # Invalid or expired API credentials
        pass
    elif status == 400:
        # Malformed request — bad phone number, empty message, etc.
        pass
    elif status == 429:
        # Rate limited — back off and retry
        pass
except requests.exceptions.Timeout:
    # Network timeout — safe to retry with backoff
    pass
except requests.exceptions.ConnectionError:
    # Upstream unreachable
    pass

As with any external API call, never surface the raw exception text to end users — log it server-side and return a clean, generic error message instead.


6. Tracking Delivery Status via Webhooks

The initial response confirms the message was accepted, not that it arrived — delivery is asynchronous. Set up a webhook endpoint to receive the final status:

@app.route("/webhooks/sms-status", methods=["POST"])
def sms_status_webhook():
    payload = request.get_json(silent=True) or {}
    message_id = payload.get("messageId")
    status = payload.get("status")  # DELIVERED, FAILED, EXPIRED

    app.logger.info("Message %s status: %s", message_id, status)
    # TODO: update your own database record for this messageId

    return "OK", 200

Register this endpoint's URL in your Sendexa dashboard so delivery events are pushed to it in real time instead of requiring you to poll.


Conclusion

That's a complete Python integration: credentials loaded safely from the environment, Basic auth handled natively by requests, input validation, a working Flask route, and webhook-based delivery tracking. The same send_sms() helper drops straight into OTP verification, order notifications, or any other transactional flow — swap the message text and destination number and you're done.

For the Node.js equivalent of this tutorial, see How to Send SMS with Node.js.

#SMS API#Python#Tutorial#Integration
Collins Vidzro

Collins Vidzro

Founder & Lead Developer at Sendexa, writing about high-throughput communication APIs, security, and digital inclusion.

Share: