How to set a group icon in Python with the Wapito WhatsApp API

Upload a square image, set it on the group, read it back for your dashboard, and clear it when the group is retired.

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.

Setting a group icon from Python is a media upload followed by a PUT: read the file with pathlib, encode it as a data URI with base64, post it, then put the media id on the group. Pillow is worth a few lines here to crop to a square before the upload, because WhatsApp will otherwise crop it for you and cut whatever sits near the edges.

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. Upload the image

    Send a square JPEG or PNG and keep the source file to hand. A rectangular image is cropped by WhatsApp rather than letterboxed, so anything with text near the edge will lose it.

    import requests
    
    url = "https://api.wapito.com/v1/media"
    
    payload = {
        "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6…",
        "filename": "launch-team.png"
    }
    headers = {
        "Authorization": "Bearer wpt_YOUR_TOKEN",
        "Content-Type": "application/json"
    }
    
    response = requests.post(url, json=payload, headers=headers)
    
    print(response.json())

    In Python build the upload body as {"data": f"data:{mime};base64,{base64.b64encode(Path(path).read_bytes()).decode()}", "filename": name} and requests.post it as json=; mimetypes.guess_type gives you the mime string. The returned dict carries the media id under id and an expires_at, which is all the next step needs before that window closes; keep the upload in one small function you can test with a tiny PNG.

    API reference for this step
  2. Set it as the group icon

    Apply the uploaded media to the group. The change is announced in the group, so avoid running this on a schedule that flips the icon back and forth.

    import requests
    
    url = "https://api.wapito.com/v1/groups/120363041234567890@g.us/icon"
    
    payload = { "media": "https://acme.example/brand/launch-team.png" }
    headers = {
        "Authorization": "Bearer wpt_YOUR_TOKEN",
        "Content-Type": "application/json"
    }
    
    response = requests.put(url, json=payload, headers=headers)
    
    print(response.json())

    In Python apply the icon with requests.put(url, json={"media": media_id}); requests.put takes json= exactly like post. Call raise_for_status() and catch requests.HTTPError for the 403 that means the linked number is not an admin, for the 415 that means the upload was not an image, and for the 404 that means the group id was mangled.

    API reference for this step
  3. Read the current icon

    Fetch the current picture to show it in your own dashboard or to check whether an admin has replaced the one your automation set.

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

    In Python read the current picture with requests.get on /groups/{id}/icon; the dict carries a url that is None when the group has no picture, so compare it with the value you stored after your PUT to detect an admin having replaced it. Cache that comparison result rather than downloading the picture every run.

    API reference for this step
  4. Remove it when the group is retired

    Clearing the icon is a cheap, visible signal that a group is closed, which works well alongside renaming it and locking it to admins only.

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

    In Python clear the icon with requests.delete on the same /icon path, which answers with no content; check response.status_code == 204 rather than calling response.json() on an empty body, which raises a JSONDecodeError. Pair it with the rename and the admin-only lock when you retire a group, in that order, so the last thing members see is the closed state.

    API reference for this step

The whole script

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

"""Group Icon with the Wapito WhatsApp API.

Upload a square image, set it on the group, read it back for your dashboard, and clear it when the group is retired.

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

import requests

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

# --- Upload the image ---
url = BASE_URL + "/media"

payload = {
    "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6…",
    "filename": "launch-team.png"
}
headers = {
    "Authorization": "Bearer " + TOKEN,
    "Content-Type": "application/json"
}

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

print(response.json())

# --- Set it as the group icon ---
url = BASE_URL + "/groups/120363041234567890@g.us/icon"

payload = { "media": "https://acme.example/brand/launch-team.png" }
headers = {
    "Authorization": "Bearer " + TOKEN,
    "Content-Type": "application/json"
}

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

print(response.json())

# --- Read the current icon ---
url = BASE_URL + "/groups/120363041234567890@g.us/icon"

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

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

print(response.json())

# --- Remove it when the group is retired ---
url = BASE_URL + "/groups/120363041234567890@g.us/icon"

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

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

print(response.status_code)

Receive the webhook

FastAPI receiver for groups — 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"]


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

  • Pillow's Image.thumbnail keeps the aspect ratio and does not square the image; use ImageOps.fit with a square size so the upload is square before WhatsApp's circular crop, and save it to a BytesIO buffer rather than back over the original file.
  • Encoding a large PNG as base64 inside a JSON string is fine for an icon but not for a video; keep icons small and let the API reject anything above the size cap rather than retrying the same file, since payload_too_large is deterministic.
  • response.json() on the 204 from the delete call raises, and a try/except that catches Exception and logs 'failed' then reports a successful clear as a failure; test the status code instead, and keep the except clause narrow enough that a real network error still surfaces with its traceback.

Frequently asked questions

What size should the image be?

Square, and large enough that the client's downscale looks clean rather than soft - a few hundred pixels on a side is plenty. WhatsApp compresses aggressively, so fine detail and small text will not survive; a simple mark on a solid background reads far better in a chat list.

Can I read the icon of a group I am not in?

No. Like everything else about a group, the icon is visible only to members. Resolving an invite code gives you the group's name and size before joining, but not its picture, so a preview screen has to make do with the metadata the code returns.

Does changing the icon notify everyone?

It appears as a system message in the group naming the admin who changed it, which every member sees in the thread. It does not usually generate a push notification, but it does take up a line in the conversation, so batch changes rather than flipping the icon repeatedly.

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.