Tutorial Factory

Powered by Telnyx

Dev Docs Low Latency Club GitHub Log in

Send Single SMS with Python and Flask

Overview

Build a production-ready Flask endpoint that sends SMS messages using the Telnyx Python SDK. This tutorial demonstrates the new client-based initialization pattern, proper error handling for telecom APIs, and secure credential management via environment variables.

Prerequisites

  • Python 3.8 or higher.
  • A Telnyx account with an active API key from the Telnyx Portal.
  • A Telnyx phone number enabled for outbound SMS.
  • pip (Python package manager).

Step 1: Setup

Install the required dependencies:

pip install telnyx flask python-dotenv

Create a project directory and navigate into it:

mkdir telnyx-sms-sender
cd telnyx-sms-sender

Step 2: Configuration

Create a .env file in your project root to store credentials securely:

TELNYX_API_KEY=YOUR_API_KEY_HERE
TELNYX_PHONE_NUMBER=+15551234567

Replace YOUR_API_KEY_HERE with your actual API key and +15551234567 with your Telnyx phone number in E.164 format.

Step 3: Implementation

Create app.py and initialize the Telnyx client using the new pattern. Define a helper function to handle message creation with proper validation:

import os
import telnyx
from dotenv import load_dotenv

load_dotenv()

# Initialize client with the new SDK pattern
client = telnyx.Telnyx(api_key=os.getenv("TELNYX_API_KEY"))


def send_sms(to_number: str, message: str) -> dict:
    """Send SMS via Telnyx and return JSON-serializable response data."""
    from_number = os.getenv("TELNYX_PHONE_NUMBER")
    if not from_number:
        raise ValueError("TELNYX_PHONE_NUMBER environment variable not set")

    # Validate E.164 format to prevent API errors
    if not to_number.startswith("+"):
        raise ValueError("Phone number must be in E.164 format (e.g., +15551234567)")

    # Use client.messages.create() with proper parameters
    response = client.messages.create(
        from_=from_number,
        to=to_number,
        text=message,
    )

    # Extract serializable data — SDK objects are NOT JSON-serializable
    return {
        "message_id": response.data.id,
        "status": response.data.to[0].status if response.data.to else "unknown",
        "from": from_number,
        "to": to_number,
    }

Step 4: Testing

Add the Flask route with comprehensive error handling for production resilience:

from flask import Flask, jsonify, request

app = Flask(__name__)


@app.route("/sms/send", methods=["POST"])
def send_sms_endpoint():
    """HTTP endpoint to send single SMS."""
    data = request.get_json()

    if not data:
        return jsonify({"error": "Request body required"}), 400

    to_number = data.get("to")
    message = data.get("message")

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

    try:
        result = send_sms(to_number, message)
        return jsonify(result), 200

    except telnyx.AuthenticationError:
        return jsonify({"error": "Invalid API key"}), 401
    except telnyx.RateLimitError:
        return jsonify({"error": "Rate limit exceeded. Please slow down."}), 429
    except telnyx.APIStatusError as e:
        return jsonify({"error": str(e), "status_code": e.status_code}), e.status_code
    except telnyx.APIConnectionError:
        return jsonify({"error": "Network error connecting to Telnyx"}), 503
    except ValueError as e:
        return jsonify({"error": str(e)}), 400


if __name__ == "__main__":
    app.run(debug=True, port=5000)

Start the server:

python app.py

Test the endpoint using curl:

curl -X POST http://localhost:5000/sms/send \
  -H "Content-Type: application/json" \
  -d '{"to": "+15559876543", "message": "Hello from Telnyx!"}'

Expected response:

{
  "message_id": "403171d8-8b00-4b8f-9e9c-1234567890ab",
  "status": "queued",
  "from": "+15551234567",
  "to": "+15559876543"
}

Complete Code

#!/usr/bin/env python3
"""Production-ready Flask endpoint for sending SMS via Telnyx."""

import os
import telnyx
from dotenv import load_dotenv
from flask import Flask, jsonify, request

load_dotenv()

app = Flask(__name__)

# Initialize client with the new SDK pattern
client = telnyx.Telnyx(api_key=os.getenv("TELNYX_API_KEY"))


def send_sms(to_number: str, message: str) -> dict:
    """Send SMS via Telnyx and return JSON-serializable response data."""
    from_number = os.getenv("TELNYX_PHONE_NUMBER")
    if not from_number:
        raise ValueError("TELNYX_PHONE_NUMBER environment variable not set")

    # Validate E.164 format to prevent API errors
    if not to_number.startswith("+"):
        raise ValueError("Phone number must be in E.164 format (e.g., +15551234567)")

    # Use client.messages.create() with proper parameters
    response = client.messages.create(
        from_=from_number,
        to=to_number,
        text=message,
    )

    # Extract serializable data — SDK objects are NOT JSON-serializable
    return {
        "message_id": response.data.id,
        "status": response.data.to[0].status if response.data.to else "unknown",
        "from": from_number,
        "to": to_number,
    }


@app.route("/sms/send", methods=["POST"])
def send_sms_endpoint():
    """HTTP endpoint to send single SMS."""
    data = request.get_json()

    if not data:
        return jsonify({"error": "Request body required"}), 400

    to_number = data.get("to")
    message = data.get("message")

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

    try:
        result = send_sms(to_number, message)
        return jsonify(result), 200

    except telnyx.AuthenticationError:
        return jsonify({"error": "Invalid API key"}), 401
    except telnyx.RateLimitError:
        return jsonify({"error": "Rate limit exceeded. Please slow down."}), 429
    except telnyx.APIStatusError as e:
        return jsonify({"error": str(e), "status_code": e.status_code}), e.status_code
    except telnyx.APIConnectionError:
        return jsonify({"error": "Network error connecting to Telnyx"}), 503
    except ValueError as e:
        return jsonify({"error": str(e)}), 400


if __name__ == "__main__":
    app.run(debug=True, port=5000)

Troubleshooting

Issue Problem Solution
Authentication Error (401) The endpoint returns {"error": "Invalid API key"} with HTTP 401. Verify your TELNYX_API_KEY in the .env file matches the key shown in the Telnyx Portal. Ensure there are no trailing spaces or quotes. If the key was regenerated recently, update your environment file and restart the Flask server.
Invalid Phone Number Format You receive a 400 error stating "Phone number must be in E.164 format" or a Telnyx API error about invalid destination. Ensure all phone numbers use E.164 format: start with +, followed by country code and number without spaces or dashes. Example: +15551234567 (US) or +447700900123 (UK). Update your test curl command to use properly formatted numbers.
Environment Variable Not Set The application raises ValueError: TELNYX_PHONE_NUMBER environment variable not set on startup or first request. Confirm your .env file exists in the same directory as app.py and contains the variable. Ensure the file is named exactly .env (not .env.txt or env). The load_dotenv() call must execute before os.getenv() is called—verify this import order in your code.

Next Steps