Python SDK

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:

Install the SDK

Add the sentdm package to your existing environment:

pip install sentdm

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 runserver

Send 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_id when 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.send call 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 text instead of template, 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

On this page