Sending messages from Django with the Sent Python SDK
This guide shows you how to wire Sent messaging into an existing Django project: install the Python SDK, configure a shared client, send a template message from a view, receive delivery webhooks, and verify the whole loop in sandbox mode.
Prerequisites
This guide assumes a working Django 4.2+ project and familiarity with views and URL configuration. You also need:
- A Sent API key from the API Keys page in your Sent Dashboard
- A public HTTPS URL for webhook delivery. For local work, open a tunnel as described in the webhook local development guide
Configure the client
Read the credentials in settings.py so every module gets them from one place; the webhook secret arrives in step 4:
# settings.py
import os
SENT_DM_API_KEY = os.environ.get("SENT_DM_API_KEY")
SENT_DM_WEBHOOK_SECRET = os.environ.get("SENT_DM_WEBHOOK_SECRET")
if not SENT_DM_API_KEY:
raise ValueError("SENT_DM_API_KEY environment variable is required.")Create one cached client for the whole process instead of building a new one per request:
# sent_integration/client.py
from functools import lru_cache
from django.conf import settings
from sent_dm import Sent
@lru_cache(maxsize=1)
def get_sent_client() -> Sent:
return Sent(api_key=settings.SENT_DM_API_KEY)Send a template message from a view
Add a JSON view that calls messages.send; the pass-through sandbox flag lets callers exercise the view without delivering anything. Protect the route with your existing API authentication (it is left open here for brevity):
# sent_integration/views.py
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from .client import get_sent_client
@csrf_exempt
@require_POST
def send_message(request):
data = json.loads(request.body)
response = get_sent_client().messages.send(
to=[data["phone_number"]], # E.164 format, for example +14155551234
template={
"name": data["template_name"], # reference by "name" or "id", never both
"parameters": data.get("parameters", {}),
},
channel=data.get("channels"), # omit to let Sent pick per recipient
sandbox=data.get("sandbox", False), # True = validate and simulate only
)
recipient = response.data.recipients[0]
return JsonResponse(
{"message_id": recipient.message_id, "status": response.data.status},
status=202,
)Sent accepts sends asynchronously: the API responds with status QUEUED and one message_id per recipient-and-channel pair. Store the message_id. Delivery outcomes arrive on your webhook endpoint instead of in this response.
Receive delivery webhooks
Add a webhook view that verifies the X-Webhook-Signature header against the raw request body before trusting any event. The scheme is HMAC-SHA256 over {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody}, keyed with the base64-decoded secret after stripping its whsec_ prefix. Refer to webhook signature verification for the full scheme:
# sent_integration/webhooks.py
import base64
import hashlib
import hmac
import json
import logging
import time
from django.conf import settings
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name="dispatch")
class WebhookView(View):
http_method_names = ["post"]
def post(self, request, *args, **kwargs):
webhook_secret = getattr(settings, "SENT_DM_WEBHOOK_SECRET", None)
if not webhook_secret:
return JsonResponse({"error": "Webhook secret not configured"}, status=500)
signature = request.headers.get("X-Webhook-Signature", "")
webhook_id = request.headers.get("X-Webhook-ID", "")
timestamp = request.headers.get("X-Webhook-Timestamp", "")
if not signature:
return JsonResponse({"error": "Missing signature"}, status=401)
# Strip the "whsec_" prefix and base64-decode to get the raw HMAC key
key_bytes = base64.b64decode(webhook_secret.removeprefix("whsec_"))
# Signed content = "{webhookId}.{timestamp}.{rawBody}"; signature format = "v1,{base64(hmac)}"
signed = f"{webhook_id}.{timestamp}.{request.body.decode('utf-8')}"
digest = hmac.new(key_bytes, signed.encode("utf-8"), hashlib.sha256).digest()
expected = "v1," + base64.b64encode(digest).decode()
if not hmac.compare_digest(signature, expected):
return JsonResponse({"error": "Invalid signature"}, status=401)
# Reject replayed events older than 5 minutes
try:
if abs(time.time() - int(timestamp)) > 300:
return JsonResponse({"error": "Timestamp too old"}, status=401)
except ValueError:
return JsonResponse({"error": "Invalid timestamp"}, status=401)
event = json.loads(request.body.decode("utf-8"))
payload = event.get("payload", {})
# Envelope: {"field": ..., "event": ..., "timestamp": ..., "payload": {...}}
if event.get("field") != "message":
return JsonResponse({"received": True})
match event.get("event"):
case "message.delivered":
logger.info("Message %s delivered", payload.get("message_id"))
case "message.failed":
logger.error("Message %s failed (status %s)",
payload.get("message_id"), payload.get("message_status"))
case "message.received":
logger.info("Inbound %s from %s: %s", payload.get("channel"),
payload.get("inbound_number"), payload.get("text"))
case _:
logger.info("Message %s status: %s",
payload.get("message_id"), payload.get("message_status"))
return JsonResponse({"received": True})Route both views in your URL configuration:
# urls.py
from django.urls import path
from sent_integration import views, webhooks
urlpatterns = [
path("api/messages/send", views.send_message),
path("webhooks/sent/", webhooks.WebhookView.as_view()),
]Then tell Sent where to deliver events. If you prefer a UI, use the webhooks getting started guide; otherwise register over the API:
curl -X POST https://api.sent.dm/v3/webhooks \
-H "x-api-key: $SENT_DM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Django integration",
"endpoint_url": "https://your-domain.example/webhooks/sent/",
"event_types": ["message"]
}'Copy two values from the response: the webhook id (used to test delivery in the next step) and signing_secret. Put the secret in SENT_DM_WEBHOOK_SECRET. The message event type covers every message event; the webhook event types reference lists all payload fields.
Verify the integration
Start the development server with your credentials loaded:
python manage.py runserverSend a sandbox message through your new view. Full validation runs, but nothing is delivered and no credits are consumed:
curl -X POST http://localhost:8000/api/messages/send \
-H "Content-Type: application/json" \
-d '{"phone_number": "+14155551234", "template_name": "welcome",
"parameters": {"name": "Ada"}, "sandbox": true}'The response should contain a message_id and "status": "QUEUED". A 400 here means the request shape is wrong: sandbox requests return real validation errors.
Now confirm webhook delivery end to end. Ask Sent to deliver a signed test event, replacing the ID with the webhook id you copied:
curl -X POST https://api.sent.dm/v3/webhooks/YOUR_WEBHOOK_ID/test \
-H "x-api-key: $SENT_DM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event_type": "message.delivered"}'Your Django log should show a Message ... delivered line, and the endpoint should have answered 200 {"received": true}. Test events travel the same signed delivery pipeline as real events, so a 401 in your log means the signing secret or verification code is wrong. Sent attempts a test event exactly once, so re-run the command after each fix.
Adapt this to your app
- If you track delivery state, persist a record keyed by
message_idwhen you send, then update it from the webhook handlers. The appendix below has a model for this. - If you send in bulk or on a schedule, move the
messages.sendcall into a background worker; the Celery integration guide shows the pattern. - If webhook processing does slow work (database writes, downstream calls), acknowledge with 200 first and hand off to a task queue so retries do not pile up; see handling webhook retries.
- To send free-form text instead of a template, pass
textinstead oftemplate, since each send carries exactly one of the two.
Appendix: production scaffolding
The numbered steps stay on the core messaging tasks. The blocks below are optional scaffolding for a production Django stack. Adapt them to your own conventions rather than adopting them wholesale.
Next steps
- Review the webhook event types reference for every payload field
- Work through the webhook production checklist before going live
- Explore the Python SDK reference for retries, timeouts, and error types
- Read the SDK best practices guide for production deployments
Python SDK
Official Python SDK for Sent. Send SMS, WhatsApp, and RCS messages with Pythonic elegance and full async support.
Sending messages from FastAPI with the Sent Python SDK
Wire the Sent Python SDK into a FastAPI app: install, configure a client dependency, send messages from a route, verify webhooks, and test with sandbox mode.