How to promote group admins in Python with the Wapito WhatsApp API

Read the current roles, promote from your own source of truth, demote what is stale, and follow role changes on the webhook.

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.

Role management from Python is short: one GET to learn who holds what, one POST to promote a list, one DELETE per demotion, all with requests and a Session. The subtlety is the creator, who comes back in the participant list with a superadmin role and must be filtered out in Python before any demotion loop, or that one call fails every run.

Before you start

  • pip install requests
  • os.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

  1. See who holds a role today

    The participant list carries each member's role, including which one is the creator. Read it before you change anything: the creator cannot be demoted, so an automation that tries will fail on exactly that member.

    import requests
    
    url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/participants"
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.get(url, headers=headers)
    
    print(response.json())

    In Python read the roll with requests.get and build a dict from identity to role with a comprehension over response.json(); the creator is the entry whose role says superadmin. Keep that dict for the whole run so promotion and demotion decisions are made against one consistent snapshot rather than three separate reads.

    API reference for this step
  2. Promote the people who should moderate

    Promotion is a single call that can carry several participants. Promote from your own source of truth - a team list, a rota, a role in your CRM - rather than from whoever happens to be talking in the group.

    import requests
    
    url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/admins"
    
    payload = { "participants": ["+15551234567"] }
    headers = {
        "Authorization": "Bearer wpt_YOUR_TOKEN",
        "Content-Type": "application/json"
    }
    
    response = requests.post(url, json=payload, headers=headers)
    
    print(response.json())

    In Python promotion is requests.post(url, json={"participants": [...]}) with every identity that should moderate according to your own list. Compute the list as a set difference against the current admins from the previous step, so you only send people who are not admins yet and the call stays small and idempotent across runs.

    API reference for this step
  3. Demote anyone who no longer needs it

    Demotion is visible to the group, so do it as part of a clear process rather than silently. The linked number must itself be an admin, and it cannot demote the group's creator.

    import requests
    
    url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/admins/+15551234567"
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.delete(url, headers=headers)
    
    print(response.status_code)

    In Python a demotion is requests.delete on /admins/{pid}, one call per person, after you have dropped the superadmin from the candidate set. Call raise_for_status() and catch requests.HTTPError so a 403 forbidden, which means the linked number lost its own admin role, is logged and stops the loop cleanly.

    API reference for this step
  4. Track role changes over the webhook

    Promotions and demotions made by anyone, including other admins on their phones, arrive as events. This is how your system learns that the linked number was demoted before its next write fails.

    Arrives on your webhook as groups.participants.

    In Python the FastAPI route sees promotions and demotions as participants events with an action naming the role change; update your role table from payload["data"] in a background task. The event that matters most is the linked number itself being demoted, which your script should read before its next write.

The whole script

Every step above in one runnable file. Save it as group-admins.py, put your token in the environment, and run it.

"""Group Admins with the Wapito WhatsApp API.

Read the current roles, promote from your own source of truth, demote what is stale, and follow role changes on the webhook.

Run it with:
    export WAPITO_TOKEN="wpt_..."
    python group-admins.py
"""
import os

import requests

BASE_URL = "https://api.wapito.com/v1"
TOKEN = os.environ["WAPITO_TOKEN"]

# --- See who holds a role today ---
url = BASE_URL + "/groups/120363041234567890@g.us/participants"

headers = {"Authorization": "Bearer " + TOKEN}

response = requests.get(url, headers=headers)

print(response.json())

# --- Promote the people who should moderate ---
url = BASE_URL + "/groups/120363041234567890@g.us/admins"

payload = { "participants": ["+15551234567"] }
headers = {
    "Authorization": "Bearer " + TOKEN,
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())

# --- Demote anyone who no longer needs it ---
url = BASE_URL + "/groups/120363041234567890@g.us/admins/+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
Every event, its payload and the retry rules

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 dict comprehension keyed on phone numbers drops the creator when the list reports them by linked identity, and the demotion loop then hits the one member it cannot demote. Key on the identity field the API gives you.
  • requests.delete with json= is accepted by the library and ignored by the API, so a demotion that seems to carry a reason or a batch sends nothing of the sort; the participant id in the path is the whole request.

Frequently asked questions

Can I promote someone who is not in the group?

No. Roles apply to participants, so the person has to be a member first. Add or invite them, wait for the participants event that confirms they actually joined, and only then promote - a promotion aimed at a non-member fails rather than adding them.

What is the difference between admin and superadmin?

An admin can change the group's settings, its icon and its membership, and can promote or demote other admins. The superadmin is the creator: they have the same powers and additionally cannot be demoted or removed by anyone else, which makes the choice of creating number a long-lived decision.

Will people be notified that they were promoted?

Yes. Role changes appear as system messages in the group, visible to everyone, and the affected person sees it in their chat. Treat promotion as a public act: it is not a quiet permission change, and demoting someone without warning is usually worth a message first.

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.