Send WhatsApp messages from Python: a working example

Send WhatsApp template messages from Python with requests and a REST API — test keys, safe retries, error handling by type and signed webhook verification.

October 28, 2026 · 5 min read

To send a WhatsApp message from Python you need an approved template, an API key and one POST with requests. This guide shows that call, how to retry it without sending twice, how to handle the errors WhatsApp's rules cause, and how to verify the signed webhooks that tell you what happened next.

SendFromChat is an independent product and is not affiliated with Meta or WhatsApp. It runs on Meta's official WhatsApp Cloud API; you keep your own WhatsApp Business account.

What was tested: the code below was assembled from our REST API reference and run against a small local mock server on Python 3.14 with requests 2.32. It was not run against a live SendFromChat key, and the Flask route in step 5 was not run at all. Test it with a test key first. It needs Python 3.10 or newer (for the str | None type hints).

The short version

  1. Get a template approved (usually minutes, sometimes up to 24 hours).
  2. Create a test key, which runs every check and sends nothing.
  3. POST /messages with an Idempotency-Key, so a retry never double-sends.
  4. Verify the SendFromChat-Signature header on webhooks before you trust them.

1. A template

A message you start outside a 24-hour conversation window must use a template Meta has approved (how the window works). A shipping update is a Utility template, for example:

Hi {{1}}, your order {{2}} has shipped.

Create it on the dashboard or ask your assistant to draft and submit it. It's checked against Meta's formatting rules first (the rules, explained). Only message people who agreed to WhatsApp updates (how to collect opt-in).

2. A key you can't hurt yourself with

On the dashboard's Developers page, create a test key (sfc_test_…). It checks your plan, opt-outs, the template and the 24-hour window, then stops: nothing is sent, saved or counted. Swap in a live key (sfc_live_…) when you go to production.

Keep the key on your server, in an environment variable. Never put it in browser or mobile-app code.

pip install requests
export SENDFROMCHAT_KEY=sfc_test_...

3. Send a template

import os
import requests

API = "https://sendfromchat.com/api/v1"


class SendFromChatError(Exception):
    def __init__(self, status: int, type_: str, message: str):
        super().__init__(f"{type_}: {message}")
        self.status = status
        self.type = type_


def send_template(to: str, name: str, variables: list[str],
                  idempotency_key: str, language: str = "en_US") -> dict:
    res = requests.post(
        f"{API}/messages",
        headers={
            "Authorization": f"Bearer {os.environ['SENDFROMCHAT_KEY']}",
            "Idempotency-Key": idempotency_key,
        },
        json={"to": to, "template": {"name": name, "language": language, "variables": variables}},
        timeout=10,
    )
    body = res.json()
    if not res.ok:
        err = body.get("error", {})
        raise SendFromChatError(res.status_code, err.get("type", "unknown"), err.get("message", ""))
    return body  # {"id": "wamid…", "status": "accepted", …}


send_template("+491512345678", "order_shipped", ["Ana", "#4821"], "order-4821-shipped")

Three details matter:

  • to is international format with the country code, such as +491512345678. If your numbers come from forms or a spreadsheet, normalise them first (phone number format, with a Python example).
  • variables fill {{1}}, {{2}}… in order.
  • The idempotency key comes from the event, not from the attempt. order-4821-shipped is the same on every retry, so a timeout or a re-run job gets the first response back instead of a second message. Reusing a key with a different body returns idempotency_mismatch.

With a test key the response is 200 with "status": "test". With a live key it is 202 accepted, which means the request was accepted, not that the customer received or read it. Check GET /messages/{id} or subscribe to webhooks for the status.

4. Handle errors by type

Errors share one shape, {"error": {"type": …, "message": …}}. Branch on type, not on the message text:

try:
    send_template("+491512345678", "order_shipped", ["Ana", "#4821"], "order-4821-shipped")
except SendFromChatError as e:
    if e.type in ("opted_out", "window_closed", "template_not_approved"):
        pass  # WhatsApp's rules, not a bug: log it and move on
    elif e.type == "warmup_limit":
        pass  # a new number hit today's cap: retry later
    elif e.status >= 500:
        pass  # safe to retry with the same Idempotency-Key
    else:
        raise

opted_out, window_closed, template_not_approved and whatsapp_rejected come back as 422. warmup_limit is a 429: brand-new numbers are capped at 50, 100, then 200 template sends a day for the first three days. The full table is in the REST API reference.

5. Know what happened next

For delivery status and customer replies, add a webhook endpoint on the Developers page and subscribe to message.status and message.received. Each request carries a header like SendFromChat-Signature: t=1790300000,v1=5f2b…, where v1 is the hex HMAC-SHA256 of <t>.<raw request body> keyed with your endpoint secret. Verify it over the raw bytes, compare in constant time and reject timestamps older than five minutes:

import hashlib
import hmac
import time


def verify_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header:
        return False
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t = int(parts["t"])
    except (ValueError, KeyError):
        return False
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Wired into Flask (this route was not run; adapt it to your framework):

from flask import Flask, request

app = Flask(__name__)


@app.post("/webhooks/sendfromchat")
def sendfromchat_webhook():
    if not verify_signature(request.get_data(), request.headers.get("SendFromChat-Signature"), WEBHOOK_SECRET):
        return "", 400
    event = request.get_json()
    # deliveries can repeat: skip event["id"] values you have already handled
    return "", 204

Read the body with request.get_data(), not request.json: re-serialised JSON no longer matches the signature. Answer with a 2xx within 5 seconds; failures are retried with backoff for about a day. The event payloads are in the REST API reference, and the webhooks guide shows the Node version.

Replies

When a customer answers, a 24-hour window opens and you can reply with free-form text, no template:

requests.post(f"{API}/messages",
              headers={"Authorization": f"Bearer {os.environ['SENDFROMCHAT_KEY']}"},
              json={"to": "+491512345678", "text": "Thanks, we're on it."}, timeout=10)

Replies, auto-replies and chatbot messages are unlimited on every plan. Only messages you start count toward the monthly allowance (plans and limits).

Costs, in plain terms

Two separate things: SendFromChat's flat monthly plan (from $19), and Meta's per-message charge, which Meta bills to your own WhatsApp Business account at its published rates, with no markup from us. Rates depend on the message category and the recipient's country; see WhatsApp API pricing.

A note for EU and UK customers

SendFromChat stores data in Mumbai, India. Transfers from the EU and UK are covered by the Standard Contractual Clauses and the UK Addendum in our Data Processing Agreement, and the sub-processors are listed publicly. Read it before you decide; our GDPR guide covers consent, opt-out and records on your side.

Try it

Start the 30-day free trial (no card), connect a number, create a test key and run the snippet above. If you build with Claude Code, you can ask it to "add WhatsApp order notifications to my app" and it writes the integration in your language (Claude Code guide). Weighing other routes? See SendFromChat vs Twilio and using Meta's Cloud API directly.

Do this from your AI assistant

SendFromChat connects Claude, ChatGPT, Cursor or any MCP client to the WhatsApp Business API — templates, bulk sends, inbox, chatbots and sequences, just by asking. 30 days free, no card.

Start free