How to create a channel in Python with the Wapito WhatsApp API

List what exists, create the channel with an honest description, store its id, and delete only when you have archived the history.

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 up a WhatsApp Channel from Python is four requests calls against the /newsletters paths, which is the protocol's own word for the broadcast object and the one you will see in every dict that comes back. A script that lists first, creates only when the name is absent, and stores the id and invite link as strings is the whole job, and it runs fine from a laptop.

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. List the channels this number owns

    Start from the live list so a create job does not make a second channel with the same name. Channels are called newsletters in the protocol, which is the vocabulary the API uses.

    import requests
    
    url = "https://api.wapito.com/v1/newsletters"
    
    querystring = {"role":"owner","count":"50","offset":"0"}
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.get(url, headers=headers, params=querystring)
    
    print(response.json())

    In Python read the existing channels with requests.get on /newsletters and build a dict from name to id with a comprehension over response.json()["newsletters"], so the create step is a simple membership test on that dict. The list is small for one number, so no paging is needed; pass role=owner as a query parameter to skip channels you merely follow.

    API reference for this step
  2. Create the channel

    Give it a name and a description that say plainly what will be published and how often. Followers cannot reply, so the description is the only place to set expectations before someone follows.

    import requests
    
    url = "https://api.wapito.com/v1/newsletters"
    
    payload = {
        "name": "Acme Release Notes",
        "description": "Every shipped change, once a week.",
        "picture": "https://acme.example/brand/channel-cover.png"
    }
    headers = {
        "Authorization": "Bearer wpt_YOUR_TOKEN",
        "Content-Type": "application/json"
    }
    
    response = requests.post(url, json=payload, headers=headers)
    
    print(response.json())

    In Python create with requests.post(url, json={"name": ..., "description": ...}); the description is the only thing a follower sees before following, so write it in a constant at the top of the script where it is easy to review. The returned dict carries the new id and invite_link; keep both, and add a picture key with the logo's media id.

    API reference for this step
  3. Read it back and store the id

    The channel id ends in its own suffix and is the recipient you post to later. Store it as a string alongside the invite link, which is what you actually publish.

    import requests
    
    url = "https://api.wapito.com/v1/newsletters/120363099887766554@newsletter"
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.get(url, headers=headers)
    
    print(response.json())

    In Python read the channel back with requests.get on /newsletters/{id} and store response.json()["id"] as a str; the id ends in its own suffix and is the recipient your later posts go to. Save the invite link next to it, because the link is what you actually publish, and note subscribers_count so a later run can report growth.

    API reference for this step
  4. Delete a channel you no longer run

    Deletion removes the channel for its followers too, so archive the posts you care about first. A channel that is finished but worth keeping is better left in place with a final post.

    import requests
    
    url = "https://api.wapito.com/v1/newsletters/120363099887766554@newsletter"
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.delete(url, headers=headers)
    
    print(response.status_code)

    In Python deletion is requests.delete on the same path, and it removes the channel for every follower at once. Archive the posts you want with the list-messages endpoint first, then call raise_for_status() and catch requests.HTTPError for the 501 engine_unsupported_feature that some engines return for this call, and for the 403 that means the number is not the owner.

    API reference for this step

The whole script

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

"""Create Channel with the Wapito WhatsApp API.

List what exists, create the channel with an honest description, store its id, and delete only when you have archived the history.

Run it with:
    export WAPITO_TOKEN="wpt_..."
    python create-channel.py
"""
import os

import requests

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

# --- List the channels this number owns ---
url = BASE_URL + "/newsletters"

querystring = {"role":"owner","count":"50","offset":"0"}

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

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

print(response.json())

# --- Create the channel ---
url = BASE_URL + "/newsletters"

payload = {
    "name": "Acme Release Notes",
    "description": "Every shipped change, once a week.",
    "picture": "https://acme.example/brand/channel-cover.png"
}
headers = {
    "Authorization": "Bearer " + TOKEN,
    "Content-Type": "application/json"
}

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

print(response.json())

# --- Read it back and store the id ---
url = BASE_URL + "/newsletters/120363099887766554@newsletter"

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

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

print(response.json())

# --- Delete a channel you no longer run ---
url = BASE_URL + "/newsletters/120363099887766554@newsletter"

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

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

print(response.status_code)

Receive the webhook

FastAPI receiver for channel — 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 = ["channel"]


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

  • The word channel in the Wapito docs means your linked number, while the object these calls manage is a newsletter; a variable named channel_id that holds a newsletter id will confuse the next reader and, eventually, you. Call it newsletter_id and let the linter enforce the name.
  • A script that makes a new channel on every run because it compares names case-sensitively against a list where the name was capitalised differently ends up with duplicates that cannot be merged; normalise with str.casefold before comparing, and strip trailing whitespace too.
  • invite_link in the create response is nullable, and an engine that fills it only on the read-back hands you None on the first call; treat a missing link as a reason to call the read step again rather than to publish an empty string, and make the column nullable so the first insert does not fail on it.

Frequently asked questions

Why does the API call it a newsletter?

Because that is the object's name in the protocol and in every engine library. WhatsApp marketed the feature to users as Channels, but the wire format never changed. Wapito keeps the protocol name in the API so the field you see matches what the engine returns, and uses channel for your linked number.

Can I see who follows my channel?

No. Follower identities are hidden from the owner by design - you see a count, not a list. That is a real difference from a group, and it means a channel is a publishing tool rather than a contact-collection tool. Put a link in your posts if you need people to identify themselves.

How many channels can one number own?

WhatsApp does not publish a figure, and creating them in bulk is exactly the pattern that draws attention. Create the channels you will actually publish to, on a warmed-up number, and space the creations out rather than provisioning a batch in one afternoon.

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.