How to approve join requests 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.
A join-request queue in Python is a polling script: turn approval on once, then on a schedule read the pending list with requests, look each identity up in your own records, and approve or reject accordingly. The read is the part to write carefully; a generator over the participants keeps the approval loop simple and makes the decision live in one place, with a counter as a guard.
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
Turn on approval for the group
Join approval is a group setting. Switch it on before you publish the invite link anywhere public, otherwise the first hour of a campaign fills the group with accounts nobody has looked at.
import requests url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/settings" payload = { "messages_admin_only": False, "membership_approval": True } headers = { "Authorization": "Bearer wpt_YOUR_TOKEN", "Content-Type": "application/json" } response = requests.patch(url, json=payload, headers=headers) print(response.json())In Python enable approval with requests.patch(url, json={"membership_approval": True}), and do it before the link goes anywhere public. Because it is a settings write, catch requests.HTTPError for the 403 that means the number is not an admin, and check the value in the returned dict afterwards rather than trusting the status code alone; the PATCH echoes all four settings.
API reference for this stepRead the pending queue
The queue lists everyone waiting, with the identity that will become the participant. Page through it rather than assuming it is short; a link that was shared widely can produce hundreds of requests overnight.
import requests url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/applications" headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.get(url, headers=headers) print(response.json())In Python write a generator that calls requests.get on /groups/{id}/applications and yields each entry of the participants list, with count telling you how many are waiting. Every consumer then reads the whole queue with a for loop, and a queue of a thousand overnight requests is handled without special code; each entry carries the id, phone, lid and name.
API reference for this stepApprove the ones you recognise
Match each request against your own list - paid subscribers, enrolled students, staff numbers - and approve only those. Approving everything defeats the point of turning the setting on.
import requests url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/applications/+15551234567/approve" headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.post(url, headers=headers) print(response.json())In Python approve with requests.post on /applications/{pid}/approve and no body, which answers 201; look the identity up first with a set built from your subscriber table so the decision is a membership test rather than a database hit per request. Log the approval with the timestamp, and keep a counter so a runaway loop cannot approve unbounded numbers.
API reference for this stepReject the rest and record why
Rejection is quiet: the person is not told why. Keep your own log so a mistaken rejection can be explained when the person asks, because WhatsApp gives them no way to appeal.
import requests url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/applications/+15551234567" headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.delete(url, headers=headers) print(response.status_code)In Python rejection is requests.delete on the same /applications/{pid} path, answered with a 204, and the person hears nothing. Write your reason into your own log at the moment you call it, with the identity and the rule that failed, because a support request a week later is answerable only from that line; a structured logger makes it searchable.
API reference for this step
The whole script
Every step above in one runnable file. Save it as group-join-requests.py, put your token in the environment, and run it.
"""Group Join Requests with the Wapito WhatsApp API.
Turn approval on, read the pending queue, approve the people you can identify, reject the rest and keep your own audit log.
Run it with:
export WAPITO_TOKEN="wpt_..."
python group-join-requests.py
"""
import os
import requests
BASE_URL = "https://api.wapito.com/v1"
TOKEN = os.environ["WAPITO_TOKEN"]
# --- Turn on approval for the group ---
url = BASE_URL + "/groups/120363041234567890@g.us/settings"
payload = {
"messages_admin_only": False,
"membership_approval": True
}
headers = {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.json())
# --- Read the pending queue ---
url = BASE_URL + "/groups/120363041234567890@g.us/applications"
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.get(url, headers=headers)
print(response.json())
# --- Approve the ones you recognise ---
url = BASE_URL + "/groups/120363041234567890@g.us/applications/+15551234567/approve"
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.post(url, headers=headers)
print(response.json())
# --- Reject the rest and record why ---
url = BASE_URL + "/groups/120363041234567890@g.us/applications/+15551234567"
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.delete(url, headers=headers)
print(response.status_code)
Receive the webhook
FastAPI receiver for groups.participants — 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 = ["groups.participants"]
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 over response.json() rather than over its participants list iterates the dict's keys and approves nothing; read the list explicitly, and re-read the queue after the loop so a request that arrived mid-run is not left waiting until tomorrow.
- Looking each request up with a separate SQL query per identity turns a large queue into thousands of round trips; load the allow-list into a set once per run and test membership in Python, refreshing it only when the run starts.
- Matching on the phone field alone misses the requests that arrive with a lid and a null phone; build the allow-list with both keys where you have them, and treat a request that matches neither as unknown rather than as a rejection, so it waits for a human.
Frequently asked questions
How long do pending requests stay in the queue?
WhatsApp expires them after a period rather than keeping them indefinitely, so a queue that is only drained weekly will lose requests. Drain it on a schedule measured in minutes or hours, and tell people roughly how long approval takes on the page where you publish the link.
Does the person know they were rejected?
They see that they are not in the group, but they are not given a reason and there is no appeal inside WhatsApp. If rejection is part of a business process - an expired subscription, for example - tell them through the channel you already have with them rather than leaving them guessing.
Can I approve everyone automatically?
You can, but then the setting is doing nothing except adding delay. Approval is worth turning on only when you have something to check against: a subscriber list, an accreditation list, a CRM segment. Otherwise leave it off and let people join directly by link.
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.