
Send SMS in Django and Flask: textbee Integration Guide
Send SMS from Django and Flask applications using textbee and your Android phone as the gateway. Service class, Celery async tasks, Django signals, Flask blueprints: zero per-message fees.
TL;DR
- A reusable
SmsServiceclass withrequestsis the foundation for both Django and Flask. - Never block the request thread with SMS sends. Always dispatch to a background task (Celery, Django-Q, or a thread).
- Django: service class + Celery task + optional signal hook. Flask: blueprint + background thread or Celery.
- Store credentials in environment variables, load them via Django's
settingsor Flask'sapp.config. - No per-message fee: textbee sends SMS through your Android SIM, not a CPaaS billing meter.
The standalone Python guide covers sending SMS from a script. This guide covers what changes when you're inside a web framework: service injection patterns, async task dispatch, framework-idiomatic configuration, and signal-driven sends.
Prerequisites
- textbee installed on your Android device: download and quickstart
- Device ID and API key from textbee.dev
- Python 3.10+
requestslibrary:pip install requests- For async:
celery+ a broker (Redis recommended):pip install celery redis
The core service class (shared by Django and Flask)
# sms/service.py (or anywhere in your project)
import os
import requests
class SmsService:
URL = "https://api.textbee.dev/api/v1/gateway/send-sms"
def __init__(self, device_id: str, api_key: str):
self.device_id = device_id
self.api_key = api_key
def send(self, recipients: str | list[str], message: str) -> dict:
response = requests.post(
self.URL,
headers={"x-api-key": self.api_key, "Content-Type": "application/json"},
json={"deviceId": self.device_id,
"recipients": [recipients] if isinstance(recipients, str) else recipients,
"message": message},
timeout=10,
)
response.raise_for_status()
return response.json()["data"]This class is framework-agnostic. The sections below show how to wire it into Django and Flask respectively.
Django integration
Configuration
Add credentials to your .env (and your secrets manager in production):
TEXTBEE_DEVICE_ID=your-device-id
TEXTBEE_API_KEY=your-api-keyReference them in settings.py:
import os
TEXTBEE_DEVICE_ID = os.environ["TEXTBEE_DEVICE_ID"]
TEXTBEE_API_KEY = os.environ["TEXTBEE_API_KEY"]Service factory (singleton pattern)
Create a factory function so the service is instantiated once and reused:
# sms/service.py
from django.conf import settings
_sms_service = None
def get_sms_service() -> SmsService:
global _sms_service
if _sms_service is None:
_sms_service = SmsService(
device_id=settings.TEXTBEE_DEVICE_ID,
api_key=settings.TEXTBEE_API_KEY,
)
return _sms_serviceUse it in views:
from sms.service import get_sms_service
class AppointmentConfirmView(View):
def post(self, request, pk):
appointment = get_object_or_404(Appointment, pk=pk)
get_sms_service().send(
appointment.client.phone,
f"Hi {appointment.client.first_name}, your appointment on {appointment.date} is confirmed.",
)
return JsonResponse({"status": "confirmed"})Don't do this in production: the requests.post call blocks the view thread and adds 1-3 seconds to response time. Use a Celery task instead (see below).
Celery async task
# sms/tasks.py
from celery import shared_task
from sms.service import get_sms_service
import logging
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
def send_sms_task(self, recipients, message):
try:
result = get_sms_service().send(recipients, message)
logger.info("SMS sent: batch=%s recipients=%d", result["smsBatchId"], result["recipientCount"])
return result
except Exception as exc:
logger.warning("SMS send failed (attempt %d): %s", self.request.retries + 1, exc)
raise self.retry(exc=exc)Dispatch from a view. The response returns immediately:
from sms.tasks import send_sms_task
class AppointmentConfirmView(View):
def post(self, request, pk):
appointment = get_object_or_404(Appointment, pk=pk)
send_sms_task.delay(
appointment.client.phone,
f"Hi {appointment.client.first_name}, your appointment on {appointment.date} is confirmed.",
)
return JsonResponse({"status": "confirmed"})Start a Celery worker:
celery -A your_project worker --loglevel=info -Q smsScheduled SMS with Celery Beat
For time-based sends (e.g., day-before appointment reminders):
# sms/tasks.py
from celery import shared_task
from django.utils import timezone
from appointments.models import Appointment
@shared_task
def send_appointment_reminders():
tomorrow = timezone.now().date() + timezone.timedelta(days=1)
appointments = Appointment.objects.filter(
date=tomorrow,
reminder_sent=False,
).select_related("client")
for appt in appointments:
send_sms_task.delay(
appt.client.phone,
f"Reminder: your appointment is tomorrow at {appt.time}. Reply YES to confirm.",
)
appt.reminder_sent = True
Appointment.objects.bulk_update(appointments, ["reminder_sent"])Register in settings.py:
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"appointment-reminders": {
"task": "sms.tasks.send_appointment_reminders",
"schedule": crontab(hour=9, minute=0), # run at 9am daily
},
}Signal-triggered SMS
Send SMS automatically when a model changes (no explicit call needed in the view):
# orders/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Order
from sms.tasks import send_sms_task
@receiver(post_save, sender=Order)
def notify_on_ship(sender, instance, created, **kwargs):
if not created and instance.status == "shipped" and instance.client.phone:
send_sms_task.delay(
instance.client.phone,
f"Your order #{instance.number} has shipped! Track it at: {instance.tracking_url}",
)Register the signal in your app's AppConfig.ready():
# orders/apps.py
class OrdersConfig(AppConfig):
name = "orders"
def ready(self):
import orders.signals # noqa: F401Django management command for bulk sends
# management/commands/send_bulk_sms.py
from django.core.management.base import BaseCommand
from sms.tasks import send_sms_task
from customers.models import Customer
import time
class Command(BaseCommand):
help = "Send a bulk SMS to all active customers"
def add_arguments(self, parser):
parser.add_argument("message", type=str)
def handle(self, *args, **options):
customers = Customer.objects.filter(
is_active=True,
phone__isnull=False,
sms_opted_in=True,
)
self.stdout.write(f"Sending to {customers.count()} customers...")
for customer in customers:
send_sms_task.delay(customer.phone, options["message"])
time.sleep(0.1) # avoid flooding the queue
self.stdout.write(self.style.SUCCESS("All tasks dispatched."))Run: python manage.py send_bulk_sms "Our summer sale starts tomorrow. Use code SUMMER20."
Flask integration
Configuration
# config.py
import os
class Config:
TEXTBEE_DEVICE_ID = os.environ["TEXTBEE_DEVICE_ID"]
TEXTBEE_API_KEY = os.environ["TEXTBEE_API_KEY"]# app/__init__.py
from flask import Flask
from config import Config
from sms.service import SmsService
sms_service: SmsService = None # set in create_app
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
global sms_service
sms_service = SmsService(
device_id=app.config["TEXTBEE_DEVICE_ID"],
api_key=app.config["TEXTBEE_API_KEY"],
)
from .routes.appointments import appointments_bp
app.register_blueprint(appointments_bp)
return appBlueprint with background thread send
For simple Flask apps without Celery, use a daemon thread so the response returns immediately:
# routes/appointments.py
import threading
from flask import Blueprint, jsonify, request, current_app
from app import sms_service
appointments_bp = Blueprint("appointments", __name__, url_prefix="/appointments")
def _send_async(app, phone, message):
with app.app_context():
try:
sms_service.send(phone, message)
except Exception as e:
app.logger.error("SMS send failed: %s", e)
@appointments_bp.post("/<int:pk>/confirm")
def confirm(pk):
appointment = get_appointment_or_404(pk)
thread = threading.Thread(
target=_send_async,
args=(
current_app._get_current_object(),
appointment.client_phone,
f"Your appointment on {appointment.date} is confirmed.",
),
daemon=True,
)
thread.start()
return jsonify({"status": "confirmed"})The app_context() push is required for Flask's application context to be available in the thread.
Flask + Celery async task
For production Flask, Celery is more reliable than raw threads:
# celery_app.py
from celery import Celery
def make_celery(app):
celery = Celery(app.import_name)
celery.conf.broker_url = app.config["CELERY_BROKER_URL"]
celery.conf.result_backend = app.config["CELERY_RESULT_BACKEND"]
class ContextTask(celery.Task):
def __call__(self, *args, **kwargs):
with app.app_context():
return self.run(*args, **kwargs)
celery.Task = ContextTask
return celery# tasks.py
from celery_app import make_celery
from app import create_app, sms_service
flask_app = create_app()
celery = make_celery(flask_app)
@celery.task(bind=True, max_retries=3)
def send_sms_task(self, recipients, message):
try:
return sms_service.send(recipients, message)
except Exception as exc:
raise self.retry(exc=exc, countdown=30)Dispatch from a route:
from tasks import send_sms_task
@appointments_bp.post("/<int:pk>/confirm")
def confirm(pk):
appointment = get_appointment_or_404(pk)
send_sms_task.delay(appointment.client_phone, "Your appointment is confirmed.")
return jsonify({"status": "confirmed"})Don't block the request: why it matters
Every requests.post() to the textbee API takes 200ms-3s depending on network conditions. In a synchronous Django or Flask view, that time is added directly to your response time.
What happens if you block:
- A 1.5s SMS send inside a 50ms view = 30× slower response
- Under any load, workers pile up waiting for SMS responses
- If the textbee API is slow or times out, your view times out
What happens with async dispatch (Celery/thread):
- View returns in
<5ms. The SMS task runs independently - SMS failures don't cause 500 errors in your API
- You can retry failed sends without re-running the full request
The Celery @shared_task(max_retries=3) setup above handles transient failures automatically.
Frequently asked questions
Can I use textbee with Django Channels?
Yes. Django Channels runs async consumers. In an async context, use asyncio.to_thread to run the blocking requests.post call without blocking the event loop:
import asyncio
from sms.service import get_sms_service
async def notify_user(phone, message):
await asyncio.to_thread(get_sms_service().send, phone, message)Or use httpx with AsyncClient instead of requests for a native async HTTP call.
How do I send SMS in a Celery task in Django?
The send_sms_task example above is the recommended pattern: @shared_task with max_retries and default_retry_delay. Call it with .delay(phone, message) anywhere in your Django code. The task runs in the Celery worker process, completely outside the request thread.
Should I use Django signals or explicit task dispatch?
Explicit dispatch (calling send_sms_task.delay() directly in the view) is more readable and easier to test. Signals are useful when you want to decouple SMS sending from the view code: for example, sending an SMS whenever an Order model's status changes, regardless of which part of the codebase made the change. Use signals when the trigger is model-level and decoupling adds clarity; use explicit dispatch when the trigger is request-level.
How do I test SMS sends without actually sending?
In Django tests, mock the requests.post call:
from unittest.mock import patch
@patch("requests.post")
def test_confirms_appointment(self, mock_post):
mock_post.return_value.ok = True
mock_post.return_value.json.return_value = {
"data": {"success": True, "smsBatchId": "test", "recipientCount": 1}
}
response = self.client.post(f"/appointments/{self.appt.pk}/confirm/")
self.assertEqual(response.status_code, 200)
mock_post.assert_called_once()
call_kwargs = mock_post.call_args.kwargs
self.assertIn(self.appt.client.phone, call_kwargs["json"]["recipients"])For Celery, use CELERY_TASK_ALWAYS_EAGER = True in test settings to run tasks synchronously in tests.
Can I send SMS from a Flask CLI command?
Yes. Flask's @app.cli.command() decorator works well for batch sends:
@app.cli.command("send-alert")
@click.argument("message")
def send_alert(message):
phones = db.session.query(User.phone).filter(User.sms_opted_in == True).all()
for (phone,) in phones:
sms_service.send(phone, message)
time.sleep(1.5)
click.echo(f"Sent to {len(phones)} users.")Run: flask send-alert "Scheduled maintenance tonight at 11pm UTC."
Keep going
- Full API reference: send, receive, webhooks, device management
- Receive SMS with webhooks: handle inbound SMS in Django/Flask routes
- Bulk SMS developer guide: CSV lists, opt-out filtering, rate limiting
- Standalone Python SMS guide: no-framework script examples
- Pricing: flat subscription, no per-message fee
Download textbee and send your first Django or Flask SMS in under 10 minutes.
You may also like

9 Best Twilio Alternatives in 2026 (Cheaper SMS APIs Compared)
Compared: Plivo, Telnyx, Vonage, Bird, Sinch, Bandwidth, AWS SNS, ClickSend, and textbee. Real 2026 pricing, pros, and cons for each alternative.

SMS Message Templates: 2FA, Order Updates, and Reminders
Ready-to-use SMS templates for 2FA, order confirmation, shipping, appointment reminders, and alerts. Copy, customize, and use with textbee.dev.

Setting Up Two-Factor Authentication (2FA) with textbee SMS Gateway
A practical guide to implementing SMS-based two-factor authentication using textbee.dev. Step-by-step instructions with code examples.