All integration guides

Python integration

Send WhatsApp messages with Django

A complete, runnable Django example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — Django talks to the API over ordinary HTTP, so you can paste this straight into an existing project.

About 5 minutes to your first delivered message

1. Setup

Create an API key in the dashboard under Accounts → your account → API keys, then make it available to your Django code as an environment variable. Never commit the key or ship it in client-side code.

pip install requests

# settings.py
SPWP_API_KEY = os.environ["SPWP_API_KEY"]
SPWP_WEBHOOK_SECRET = os.environ["SPWP_WEBHOOK_SECRET"]

2. Send a text message

One POST with a recipient and a body. The API replies 202 Accepted with a message id in QUEUED state; delivery happens asynchronously and you can track it by id or webhook.

# notifications/spwp.py
import requests
from django.conf import settings

API_BASE = "https://api.spwp.app/api/v1"


def send_whatsapp_text(to: str, text: str) -> dict:
    res = requests.post(
        f"{API_BASE}/public/messages/text",
        headers={"X-API-Key": settings.SPWP_API_KEY},
        json={"to": to, "text": text},
        timeout=10,
    )
    res.raise_for_status()
    return res.json()["data"]


# Call it from a signal, a view, or a Celery task:
#   send_whatsapp_text(order.customer_phone, f"Order {order.id} shipped.")

3. Send an image, document or other media

Media endpoints take the same shape, with a source that is either a public HTTPS URL we fetch server-side or a base64 data URL you upload inline. Swap the path segment for video, audio, document or location.

def send_whatsapp_document(to: str, url: str, file_name: str) -> dict:
    res = requests.post(
        f"{API_BASE}/public/messages/document",
        headers={"X-API-Key": settings.SPWP_API_KEY},
        json={
            "to": to,
            "source": url,
            "fileName": file_name,
            "mimeType": "application/pdf",
        },
        timeout=30,
    )
    res.raise_for_status()
    return res.json()["data"]

4. Receive replies and delivery receipts

Inbound messages and status changes arrive as signed POSTs to your webhook URL. Recompute the HMAC over the timestamp and the raw request body — re-serialised JSON will not match — and reject anything older than five minutes.

# notifications/views.py
import hmac, hashlib, json, time
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def spwp_webhook(request):
    ts = request.headers.get("X-WA-Timestamp")
    sig = request.headers.get("X-WA-Signature", "")
    v1 = next((p[3:] for p in sig.split(",") if p.startswith("v1=")), None)
    if not ts or not v1:
        return HttpResponseBadRequest()

    if abs(time.time() * 1000 - int(ts)) > 5 * 60_000:
        return HttpResponseBadRequest()

    expected = hmac.new(
        settings.SPWP_WEBHOOK_SECRET.encode(),
        f"{ts}.{request.body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(v1, expected):
        return HttpResponseForbidden()

    event = json.loads(request.body)
    # …hand off to a task queue and return immediately…
    return HttpResponse(status=200)

Error handling

Errors carry a non-2xx status and a stable code string you can branch on: VALIDATION_FAILED, INVALID_API_KEY, ACCOUNT_NOT_CONNECTED, RATE_LIMITED, MEDIA_FETCH_FAILED. Retry on 429 while honouring Retry-After; treat 401 as a configuration problem rather than a transient one.

Next steps

The full reference covers every endpoint, field and error code, plus the webhook payload shapes.

Other languages and frameworks