Sending messages from Flask with the Sent Python SDK
This guide shows you how to wire Sent messaging into an existing Flask app: install the Python SDK, configure a shared client, send a template message from a route, receive delivery webhooks, and verify the whole loop in sandbox mode.
Prerequisites
This guide assumes a working Flask 3.x app and familiarity with blueprints. 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
Set your credentials as environment variables so they stay out of code; the webhook secret arrives in step 4:
export SENT_DM_API_KEY="your-api-key"
export SENT_DM_WEBHOOK_SECRET="whsec_your_signing_secret"Create one shared client and cache it on Flask's request context; Sent() reads SENT_DM_API_KEY by default:
# app/sent_client.py
from flask import g
from sent_dm import Sent
def get_sent_client() -> Sent:
if "sent_client" not in g:
g.sent_client = Sent()
return g.sent_clientSend a template message from a route
Add a blueprint route that calls messages.send; the pass-through sandbox flag lets callers exercise the route without delivering anything:
# app/messages.py
from flask import Blueprint, jsonify, request
from app.sent_client import get_sent_client
messages_bp = Blueprint("messages", __name__, url_prefix="/api/messages")
@messages_bp.post("/send")
def send_message():
data = request.get_json(force=True)
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 jsonify({"message_id": recipient.message_id, "status": response.data.status}), 202Register the blueprint wherever you create your app:
app.register_blueprint(messages_bp)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 decorator that verifies the X-Webhook-Signature header against the raw request body before your handler trusts 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. See webhook signature verification for the full scheme:
# app/webhook_auth.py
import base64
import functools
import hashlib
import hmac
import os
import time
from flask import request, jsonify
def verify_webhook_signature(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
signature = request.headers.get('X-Webhook-Signature')
if not signature:
return jsonify({'error': 'Missing webhook signature'}), 401
webhook_secret = os.environ.get('SENT_DM_WEBHOOK_SECRET')
if not webhook_secret:
return jsonify({'error': 'Webhook secret not configured'}), 500
webhook_id = request.headers.get('X-Webhook-ID', '')
timestamp = request.headers.get('X-Webhook-Timestamp', '')
# 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.get_data().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 jsonify({'error': 'Invalid signature'}), 401
# Reject replayed events older than 5 minutes
try:
if abs(time.time() - int(timestamp)) > 300:
return jsonify({'error': 'Timestamp too old'}), 401
except ValueError:
return jsonify({'error': 'Invalid timestamp'}), 401
return f(*args, **kwargs)
return decorated_functionAdd the endpoint itself. Every event arrives in the same envelope (field, event, timestamp, payload), so one handler routes all of them; return 200 quickly and do slow work elsewhere:
# app/webhooks.py
import logging
from flask import Blueprint, jsonify, request
from app.webhook_auth import verify_webhook_signature
logger = logging.getLogger(__name__)
webhooks_bp = Blueprint("webhooks", __name__, url_prefix="/webhooks")
@webhooks_bp.post("/sent")
@verify_webhook_signature
def handle_sent_webhook():
event = request.get_json(force=True)
payload = event.get("payload", {})
if event.get("field") != "message":
logger.info("Unhandled webhook field: %s", event.get("field"))
return jsonify({"received": True}), 200
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 jsonify({"received": True}), 200Register webhooks_bp the same way as the messages blueprint, 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": "Flask 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, which goes 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 app with your credentials loaded:
flask runSend a sandbox message through your new route (full validation runs, but nothing is delivered and no credits are consumed):
curl -X POST http://localhost:5000/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 Flask 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 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 process asynchronously 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. - If you need request validation, rate limiting, or security headers around these routes, the appendix below has the scaffolding.
Appendix: production scaffolding
The numbered steps stay on the core messaging tasks. The blocks below are optional deployment scaffolding for a Flask 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
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.
Sending Sent messages from Celery background tasks
Send Sent messages from Celery tasks: install the Python SDK, configure a worker-safe client, add a retrying send task, and verify the task with sandbox mode.