How to Send WhatsApp Messages With Python (Cloud API)

Send WhatsApp messages with Python and the Cloud API: text, template and image examples with requests, error codes, and a small Flask webhook for replies.

Illustration: a Python script next to a phone receiving a WhatsApp order update (sample data)

To send a WhatsApp message with Python, POST a JSON body to https://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messages with the requests library and a Bearer access token. You don’t need a WhatsApp-specific library: Meta’s Cloud API is plain HTTPS and JSON, and requests plus Flask for the webhook is all it takes.

This tutorial gives working Python for a text message, a template message, an image by link, error handling, and a minimal Flask webhook that verifies Meta and receives replies. The request formats follow Meta’s documentation for Graph API v26.0, the current version as of November 2026.

Key takeaways

  • You need a phone number ID, an access token and a recipient who has opted in. No extra library beyond requests.
  • Every message is the same call: requests.post(URL, headers=HEADERS, json=payload) with a different type.
  • Outside the 24-hour window, only an approved template can be sent.
  • Read error.code from failed responses; 190, 131047 and 132001 cover most first-day problems.
  • The webhook needs a GET handler for verification and a POST handler that checks the signature and returns 200 quickly.

What you need before you start

Set up a send helper

Save this at the top of a file called whatsapp.py; every snippet in this guide, including the webhook, goes in the same file. Each row is one line of Python. It reads your settings from environment variables and gives you one function to send any message type.

LineWhat it does
import os, hmac, hashlib, requestsThe standard library plus requests (hmac and hashlib are for the webhook later)
TOKEN = os.environ["WHATSAPP_TOKEN"]Your system-user access token
PHONE_NUMBER_ID = os.environ["PHONE_NUMBER_ID"]The ID of the number you send from
API_VERSION = "v26.0"Graph API version; update it when Meta releases a new one
URL = f"https://graph.facebook.com/{API_VERSION}/{PHONE_NUMBER_ID}/messages"The Messages endpoint
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}The two headers in Meta’s examples
def send(payload): return requests.post(URL, headers=HEADERS, json=payload, timeout=10)Sends any message body and returns the response

Meta released v26.0 on 29 July 2026 and uses it in its examples. Check the Graph API changelog for the latest version when you build, and keep API_VERSION in one place so upgrading is a one-line change.

Send a text message

A text message works only inside the 24-hour customer service window, which opens when the customer messages you. The body follows Meta’s format exactly:

StepPython
Sendr = send({"messaging_product": "whatsapp", "recipient_type": "individual", "to": "919800000000", "type": "text", "text": {"preview_url": False, "body": "Hi Priya, your table for 4 is booked for 8 pm today."}})
Checkprint(r.status_code, r.json())
Save the message ID (only when the send worked)message_id = r.json()["messages"][0]["id"] if r.ok else None
  • to is the number with its country code, written as digits as in Meta’s examples, such as 919800000000.
  • preview_url set to True asks WhatsApp to try to show a preview of the first link in body.
  • The message ID (wamid…) is what delivery and read statuses refer to later, so store it.

A successful response means Meta accepted the message, not that it was delivered. Delivery arrives later at your webhook. Outside the window you’ll get error 131047; send a template instead. More on the window in the WhatsApp 24-hour rule.

Send a template message

Templates are the only messages you can send to start a conversation or after the window closes. The template must be approved in the language you request. For a template named order_update with the body “Hi {{1}}, your order {{2}} has shipped.”:

PartPython
Body variablesparams = [{"type": "text", "text": "Priya"}, {"type": "text", "text": "ORD-1042"}]
Sendr = send({"messaging_product": "whatsapp", "to": "919800000000", "type": "template", "template": {"name": "order_update", "language": {"code": "en"}, "components": [{"type": "body", "parameters": params}]}})
No variablesLeave out components, as with Meta’s sample hello_world template in a new account

Values go in the same order as {{1}}, {{2}}. Templates created with named parameters take a parameter_name on each value instead. Headers with media, URL buttons, quick-reply payloads and coupon codes are all extra components; the JSON for each is in how to send a WhatsApp template message with the API, and it drops straight into the same send() call.

Inside the 24-hour window you can send an image from a public URL with an optional caption of up to 1,024 characters:

StepPython
Sendr = send({"messaging_product": "whatsapp", "recipient_type": "individual", "to": "919800000000", "type": "image", "image": {"link": "https://example.com/menu.jpg", "caption": "This week’s menu"}})
  • The link must be on a public server. Images can be JPEG, PNG or static WebP, up to 5 MB.
  • Meta recommends uploading the file and sending its media ID instead of a link, for better performance. Swap "link" for "id".
  • Documents and video work the same way with "type": "document" or "video". Audio uses "type": "audio" too, but takes no caption.

Uploading media, every media type’s limits and media errors are covered in how to send images and documents with the WhatsApp API.

Handle errors

A failed call returns a JSON error object. Build your handling around the integer code, not the title in message, which Meta says will eventually be deprecated. error.error_data.details explains the likely cause, and fbtrace_id helps Meta’s support find the request.

PythonWhy
err = {} if r.ok else r.json().get("error", {})Read the error object (empty when the send worked)
if err: print(err.get("code"), err.get("error_data", {}).get("details"))Log the code and Meta’s explanation
CodeMeaningWhat to do
190Access token expiredUse a permanent system-user token
100Unsupported or misspelled parameterCompare your JSON with Meta’s reference, key by key
131047More than 24 hours since the customer last messaged youSend an approved template
132001Template doesn’t exist in that language or isn’t approvedCheck the name, language code and status
130429Throughput limit reachedSlow down and retry later, with a growing delay

The full list is in WhatsApp API error codes. Some failures only show up later, as a failed status at your webhook.

Receive replies with a small Flask webhook

To get customer messages and delivery statuses, Meta POSTs to a webhook URL you own. This minimal Flask app handles both parts. Add it to the same whatsapp.py, below the send helper. Indentation rule: rows marked “Inside verify” or “Inside receive” are indented 4 spaces under that function; every other row starts at the left margin.

WherePython
Topimport os, hmac, hashlib (already there if you kept the helper’s first line)
Topfrom flask import Flask, request, abort
Topdef handle(messages, statuses): print(messages, statuses), a stub to replace with your own code or a queue
TopVERIFY_TOKEN = os.environ["VERIFY_TOKEN"] and APP_SECRET = os.environ["APP_SECRET"]
Topapp = Flask(__name__)
Verification@app.get("/webhook") then def verify():
Inside verifyif request.args.get("hub.mode") == "subscribe" and request.args.get("hub.verify_token") == VERIFY_TOKEN: return request.args.get("hub.challenge", ""), 200
Inside verifyabort(403)
Events@app.post("/webhook") then def receive():
Inside receiveexpected = "sha256=" + hmac.new(APP_SECRET.encode(), request.get_data(), hashlib.sha256).hexdigest()
Inside receiveif not hmac.compare_digest(request.headers.get("X-Hub-Signature-256", ""), expected): abort(401)
Inside receivevalues = [c["value"] for e in request.get_json().get("entry", []) for c in e.get("changes", [])]
Inside receivefor v in values: handle(v.get("messages", []), v.get("statuses", []))
Inside receivereturn "OK", 200
  1. Verification: when you save the callback URL in the App Dashboard, Meta sends a GET with hub.mode, hub.verify_token and hub.challenge. Return the challenge with 200 if the token matches.
  2. Signature: each POST has an X-Hub-Signature-256 header, an HMAC-SHA256 of the raw body made with your app secret. request.get_data() gives the raw bytes, and hmac.compare_digest compares safely.
  3. Messages and statuses: customer messages are in value["messages"] (text in m["text"]["body"], sender in m["from"]), and your delivery updates in value["statuses"]. Loop over every entry and change; one POST can hold several.
  4. Speed: write handle() so it only queues the work, then return 200. Slow responses lead to retries and duplicates, so de-duplicate by message ID.

To try it, set the environment variables and run flask --app whatsapp run --port 8000. Meta can only reach a public HTTPS URL, so put the app behind a public address with a valid certificate and save that URL plus /webhook as the callback URL in the App Dashboard. Flask’s built-in server is for development. In production, run the app behind a proper WSGI server on HTTPS with a valid certificate; Meta doesn’t accept self-signed certificates. Payloads, statuses and retries are explained in WhatsApp Cloud API webhooks, and if events don’t show up, work through WhatsApp webhook not receiving messages.

Is there a Python library, and is it free?

This guide uses no WhatsApp-specific package, deliberately. Meta documents the Cloud API as HTTP requests, so requests covers everything and you always match Meta’s current format. Be careful with packages that automate WhatsApp Web or a personal account: they aren’t the official API and can get numbers banned; see unofficial WhatsApp APIs.

Using the Cloud API from Python costs nothing extra: Meta hosts it without a platform fee, and what you pay for is mainly the template messages you send, charged by category and country. The details are in is the WhatsApp API free?.

Python with Automate AI

If you’d rather not run the WhatsApp side yourself, Automate AI connects your number, hosts Meta’s webhooks and gives your team a shared inbox, campaigns and chatbots. Your Python code can still send messages and templates and manage contacts through Automate AI’s REST API, with SDKs that include Python, and receive developer webhooks such as message received and message status update. The API, SDKs and webhooks are included on Series 2, 3 and 4; see the WhatsApp Business API page and pricing.

Frequently asked questions

How do I send a WhatsApp message with Python?

Use requests.post to https://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messages with a Bearer token and a JSON body containing messaging_product: "whatsapp", to, type and the message content.

Do I need a Python library for the WhatsApp API?

No. The Cloud API is plain HTTPS and JSON, so requests is enough. Avoid packages that automate WhatsApp Web; they aren’t the official API.

Can I send a WhatsApp message with Python to someone who hasn’t messaged me?

Only with an approved template, and only to people who have opted in. Free-form text, images and buttons need an open 24-hour window.

Why does my Python script get error 190?

The access token has expired. The token in API Setup is temporary; generate a permanent system-user token in Business Settings.

How do I receive WhatsApp messages in Python?

Run a webhook, for example with Flask: answer Meta’s verification GET with the challenge, check the X-Hub-Signature-256 signature on each POST, read value["messages"], and return 200 quickly.

Which Graph API version should I use?

The current one. As of November 2026 that’s v26.0, released on 29 July 2026. Keep the version in one setting so upgrades are easy.

Sources

  1. Meta: Send messages
  2. Meta: Text messages
  3. Meta: Image messages
  4. Meta: Templates overview
  5. Meta: Error codes
  6. Meta: Create a webhook endpoint
  7. Meta: Graph API changelog

WhatsApp rules and prices change. We check these guides against Meta’s documentation, but always confirm current terms with Meta before you rely on them. Questions? Message us on WhatsApp at +91 836 977 8812.