How to follow a channel 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.
Following Channels from Python is mostly reading: a requests.get to search, another to resolve an invite code, a paged read of the history, and a FastAPI route for new posts. A generator over the message pages and a dict of processed ids keep the bridge idempotent, which matters because a re-run after an outage will see the same posts again. Nothing here sends, so the ban risk is low.
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
Search the directory
Search returns public channels matching a term. Treat the results as candidates rather than as matches: names are not unique and a lookalike channel is a real risk when you are following on a customer's behalf.
import requests url = "https://api.wapito.com/v1/newsletters/find" querystring = {"q":"release notes","country":"GB","count":"50"} headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.get(url, headers=headers, params=querystring) print(response.json())In Python search with requests.get(url, params={"q": term}) so requests encodes the term; the response is a list of candidate dicts, and a name match is not proof of identity. Print the name, subscribers_count, verified and id for each and choose in code by an explicit rule rather than taking index zero; a country parameter narrows a common name.
API reference for this stepResolve an invite link
If you already have a channel link, resolve its code to get the name, description and follower count before doing anything else. This is how you confirm you have the official channel and not an imitation.
import requests url = "https://api.wapito.com/v1/newsletters/invite/0029VaAbCdEfGhIjKlMnOp" headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.get(url, headers=headers) print(response.json())In Python resolve a link by passing its code through urllib.parse.quote into /newsletters/invite/{code} and read the name, description and subscribers_count from the dict; compare them against what you were given before doing anything else. This lookup is how the script tells the official channel from an imitation, and the verified flag settles most cases on its own.
API reference for this stepRead what has been published
Page back through the channel's posts to seed your own archive or to catch up after an outage. Store the message ids so a re-run does not process the same post twice.
import requests url = "https://api.wapito.com/v1/newsletters/120363099887766554@newsletter/messages" querystring = {"count":"50","offset":"0"} headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.get(url, headers=headers, params=querystring) print(response.json())In Python page back through /newsletters/{id}/messages with a generator that yields each message and advances the offset by count until total is reached; write each message id into a set persisted with json.dump so a re-run resumes from where it stopped. Store the newest id you have seen and page forward from it on later runs.
API reference for this stepReact to new posts
New posts arrive as message events from the channel id. Route them by that id so a bridge to another system knows which feed a post belongs to.
Arrives on your webhook as
messages.In Python the FastAPI handler receives messages events whose sender is the newsletter id; route on that id with a dict from id to destination so a bridge knows which feed the post belongs to. Return the 2xx first and forward the post from a background task that checks the persisted id set, so a redelivered event posts once.
The whole script
Every step above in one runnable file. Save it as follow-channels.py, put your token in the environment, and run it.
"""Follow Channels with the Wapito WhatsApp API.
Search or resolve a link, verify it is the right channel, page back through the history, then handle new posts from the webhook.
Run it with:
export WAPITO_TOKEN="wpt_..."
python follow-channels.py
"""
import os
import requests
BASE_URL = "https://api.wapito.com/v1"
TOKEN = os.environ["WAPITO_TOKEN"]
# --- Search the directory ---
url = BASE_URL + "/newsletters/find"
querystring = {"q":"release notes","country":"GB","count":"50"}
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())
# --- Resolve an invite link ---
url = BASE_URL + "/newsletters/invite/0029VaAbCdEfGhIjKlMnOp"
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.get(url, headers=headers)
print(response.json())
# --- Read what has been published ---
url = BASE_URL + "/newsletters/120363099887766554@newsletter/messages"
querystring = {"count":"50","offset":"0"}
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())
Receive the webhook
FastAPI receiver for messages — 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"]
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
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 set of processed ids kept only in memory is lost on restart, and the bridge reposts the whole archive; persist it, or store the newest id and compare ordering, and load it once at startup rather than on every event.
- requests.get with the term concatenated into the URL string breaks on spaces and non-ASCII names; pass params= and let requests encode it, which also keeps a plus sign in a search term from being read as a space.
- json.dump of the id set on every event rewrites the whole file and a crash mid-write leaves it truncated, so the next start finds invalid JSON and starts from nothing; write to a temporary file and os.replace it, or keep the ids in SQLite with a unique constraint.
Frequently asked questions
Can I follow a private channel?
Only if you have its invite link, which is how private channels are shared in the first place. There is no way to discover a private channel through search, and resolving a code you were not given will simply fail. Treat a channel link like any other credential.
Do I get every post as a webhook?
New posts from channels the linked number follows arrive as message events keyed to the channel id, so yes for anything published after you follow. History is a separate problem: page back through the messages endpoint once, then rely on the webhook for everything after that.
Is it legal to republish what I read?
That is a copyright and terms question rather than an API one, and the answer depends on the publisher and your jurisdiction. Reading a public channel to trigger your own workflow is uncontroversial; republishing someone else's posts wholesale is a decision to take with your own legal advice.
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.