How to send a group message in Python with the Wapito WhatsApp API
A script, a token and an HTTP client is the whole stack. Everything below runs from a file you can execute today, and the same code moves to a worker or a container without changing shape.
Messaging a group from Python is the same requests.post you use for a person, with the group id as the recipient; the interesting Python is around it, not in it. Sends have to be sequential with a pause, media is uploaded once and referenced by id, and the FastAPI receiver needs to aggregate per-participant receipts rather than write each one to the database.
Before you start
pip install requestsos.environ["WAPITO_TOKEN"]- A channel with a WhatsApp number linked to it, and its API token. Create one in the dashboard — the authentication guide shows where the token goes.
How it works
Send text to the group id
There is no separate group endpoint: you send to the group's id exactly as any other recipient. Mentioning participants inside the body is what turns a message into a notification for them specifically.
import requests url = "https://api.wapito.com/v1/messages/text" payload = { "to": "+15551234567", "body": "Your order #4182 has shipped. Track it here: https://acme.example/t/4182", "typing_time": 3 } headers = { "Authorization": "Bearer wpt_YOUR_TOKEN", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers) print(response.json())In Python post text with requests.post(url, json={"to": group_id, "text": ...}) and read the message id from response.json(); the id is what the status webhook will key on, so store it. Mentions are part of the body payload, so build them in the dict rather than concatenating handles into the text string.
API reference for this stepAttach an image or a document
Upload once and reuse the media id across groups rather than re-uploading the same file for each. A caption on the image carries far better than a separate text message immediately afterwards.
import requests url = "https://api.wapito.com/v1/messages/image" payload = { "to": "+15551234567", "media": "https://acme.example/labels/4182.png", "caption": "Your shipping label for order #4182" } headers = { "Authorization": "Bearer wpt_YOUR_TOKEN", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers) print(response.json())In Python send an image by media id in the same recipient-plus-payload shape, with the caption in the dict; when the file is local, upload it once with a separate call and reuse the returned id in a loop over groups. requests handles the JSON encoding; there is no multipart form in this step.
API reference for this stepAsk with a poll rather than free text
In a group, free-text replies from dozens of people are unusable. A poll returns structured votes keyed to the participant, which a bot can count without guessing what somebody meant.
import requests url = "https://api.wapito.com/v1/messages/poll" payload = { "to": "120363041234567890@g.us", "title": "When should we run the launch standup?", "options": ["Monday 09:00", "Tuesday 10:00", "Wednesday 16:00"], "multiple": False } headers = { "Authorization": "Bearer wpt_YOUR_TOKEN", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers) print(response.json())In Python a poll is requests.post on /messages/poll with the question and an options list in the json= dict; the vote events arrive later keyed to the message id you get back. Keep that id in a dict from poll to group so a vote can be counted against the right question.
API reference for this stepWatch delivery and replies
Group delivery receipts arrive per participant, so a status event stream from a large group is noisy. Aggregate rather than storing every receipt, and use the messages event for the replies you actually want.
Arrives on your webhook as
messages.status.In Python the FastAPI handler receives one messages.status event per participant; increment a counter keyed by message id in Redis or a dict guarded by a lock rather than inserting a row per receipt. Return the 2xx immediately and let a background task flush the aggregate on a timer.
The whole script
Every step above in one runnable file. Save it as send-group-message.py, put your token in the environment, and run it.
"""Send Group Message with the Wapito WhatsApp API.
Address the group id like any recipient, attach media by id, ask questions as polls, and aggregate the per-participant delivery receipts.
Run it with:
export WAPITO_TOKEN="wpt_..."
python send-group-message.py
"""
import os
import requests
BASE_URL = "https://api.wapito.com/v1"
TOKEN = os.environ["WAPITO_TOKEN"]
# --- Send text to the group id ---
url = BASE_URL + "/messages/text"
payload = {
"to": "+15551234567",
"body": "Your order #4182 has shipped. Track it here: https://acme.example/t/4182",
"typing_time": 3
}
headers = {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
# --- Attach an image or a document ---
url = BASE_URL + "/messages/image"
payload = {
"to": "+15551234567",
"media": "https://acme.example/labels/4182.png",
"caption": "Your shipping label for order #4182"
}
headers = {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
# --- Ask with a poll rather than free text ---
url = BASE_URL + "/messages/poll"
payload = {
"to": "120363041234567890@g.us",
"title": "When should we run the launch standup?",
"options": ["Monday 09:00", "Tuesday 10:00", "Wednesday 16:00"],
"multiple": False
}
headers = {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
Receive the webhook
FastAPI receiver for messages, messages.status, polls — it verifies the signature, answers immediately and does the work after. Install it with pip install fastapi uvicorn and save it as webhook.py.
import hashlib
import hmac
import os
import time
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SECRET = os.environ["WAPITO_WEBHOOK_SECRET"].encode()
EVENTS = ["messages", "messages.status", "polls"]
def verify(raw: bytes, header: str | None) -> bool:
"""Checks the X-Wapito-Signature header: t=<ms>,v1=<hex hmac-sha256>."""
if not header:
return False
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
ts, sig = parts.get("t"), parts.get("v1")
if not ts or not sig:
return False
if abs(time.time() * 1000 - int(ts)) > 300_000: # 5 minute clock skew
return False
expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
@app.post("/wapito")
async def wapito(request: Request, signature: str | None = Header(default=None, alias="X-Wapito-Signature")):
raw = await request.body()
if not verify(raw, signature):
raise HTTPException(status_code=401, detail="bad signature")
payload = await request.json()
if payload["event"] in EVENTS:
print(payload["event"], payload["data"])
return {"ok": True} # answer 2xx fast; do the real work in a queue
Errors you may hit
Gotchas
- requests does not raise on a 4xx by itself. Call response.raise_for_status() or check response.ok, otherwise a 429 send_rate_limited silently looks like a successful send and your loop keeps hammering the queue.
- json= and data= are not interchangeable. Pass json={...} so requests sets Content-Type: application/json and encodes UTF-8 for you; data= with a dict sends form encoding and the API answers 400 invalid_request.
- The default requests timeout is None, which means a stalled connection hangs your worker forever. Always pass timeout=(5, 30) and treat a read timeout as "maybe delivered" - re-check with the message id before you send again.
- A group id such as 120363000000000000@g.us is a string, not a number. Building it with an f-string from an int drops the precision and you get 404 not_found.
- Emoji and accented text need no extra work in Python 3, but reading a CSV of numbers with the default encoding on Windows will mangle them - open files with encoding="utf-8".
Where to run it
- Google Cloud Run (container, scale to zero, one revision per deploy)
- Render background worker for the sender, Render web service for the webhook receiver
- A 1 GB VPS with systemd for the worker and Caddy in front of the FastAPI receiver
- AWS Lambda + API Gateway for the webhook receiver only (the sender needs a long-lived process for queue spacing)
Pitfalls in Python
- A for loop that posts to twenty groups without time.sleep between calls is exactly the burst the send queue rate-limits; the later posts come back 429 and a naive retry loop makes it worse.
- Inserting every status event with a synchronous cursor.execute inside the FastAPI handler blocks the event loop under a burst of receipts from a large group; aggregate in memory and write in batches.
- Building a mention by formatting the phone number into the text does nothing; the mention has to be in the structured payload the endpoint accepts, and the participant's current identity is what it needs.
Frequently asked questions
Is there a separate endpoint for group messages?
No, and that is deliberate. Every send endpoint takes a recipient, and a group id is simply one of the recipient forms it accepts. The same call that messages a person messages a group, which means your sending code does not need a special case for groups at all.
How do I mention someone in a group message?
Include the mention in the message body using the participant's identity, and the client renders it as a tap-able name that notifies them. Getting the identity right matters more than it used to, because in newer groups participants may be represented by a linked identity rather than a phone number.
Can I send to many groups at once?
Only by sending to each one in turn. The send queue serialises per channel deliberately, so a fan-out across fifty groups will be paced rather than parallel, and pushing harder returns a saturation error instead of sending faster. Build the loop to expect that pacing.
Related
Try it on your own number
Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.