How to link a number 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.
Connecting a number from Python is a state machine driven by requests and a FastAPI route: read the channel state, ask for a pairing code while the person is on the phone, stream the QR only as a fallback, and let the channel event decide what happens next. A small loop with time.sleep polls the state; the webhook does the rest.
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
Check the channel state
Read the state before you ask for anything. A code can only be issued while the session is waiting to be linked, so polling for one against a connected channel just produces errors.
import requests url = "https://api.wapito.com/v1/channel" headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.get(url, headers=headers) print(response.json())In Python read the state with requests.get on /channel and branch on response.json()["state"] before asking for anything; a code can only be issued in the waiting state. Wrap it in a small poll loop with time.sleep(2) that stops when the state is connected or the attempt count runs out.
API reference for this stepRequest a pairing code
Ask for a code for the number you intend to link, and have the person type it into the linked-devices screen on that phone. Codes expire quickly, so request one while they are already looking at the phone.
import requests url = "https://api.wapito.com/v1/channel/pairing-code" payload = { "phone": "+15557654321" } headers = { "Authorization": "Bearer wpt_YOUR_TOKEN", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers) print(response.json())In Python request a code with requests.post(url, json={"phone": e164}) and print it immediately for the person to type; the code expires fast, so the call belongs inside the same interactive step, not in a setup script that ran earlier. Catch requests.HTTPError for a 409 channel_not_in_qr_state and re-read the state.
API reference for this stepFall back to a QR if you need to
The QR refreshes every few seconds, so stream it to the browser rather than emailing a screenshot. New-device QR linking has been unreliable across engines since mid-2026, which is why the code is the primary path.
import requests url = "https://api.wapito.com/v1/channel/qr" headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"} response = requests.get(url, headers=headers) print(response.json())In Python fetch the QR with requests.get on /channel/qr and hand the image data to a browser through a streaming endpoint or a websocket, refreshing every few seconds; do not write it to a file and email it. A 409 means the channel left the waiting state while you were fetching.
API reference for this stepWatch the connection over the webhook
Every state change arrives as an event carrying the new state, the previous one and a reason. A ban needs a human and a restart does not, so branch on the reason rather than treating every disconnect the same.
Arrives on your webhook as
channel.In Python the FastAPI handler receives channel events with the new state, the previous one and a reason; branch on the reason string, because a ban needs a person and a restart does not. Write the transition to your database from a background task and return the 2xx.
The whole script
Every step above in one runnable file. Save it as connect-number-qr-pairing.py, put your token in the environment, and run it.
"""Connect a Number with the Wapito WhatsApp API.
Read the state, request a pairing code, use the QR only as a fallback, and drive everything else from the channel webhook.
Run it with:
export WAPITO_TOKEN="wpt_..."
python connect-number-qr-pairing.py
"""
import os
import requests
BASE_URL = "https://api.wapito.com/v1"
TOKEN = os.environ["WAPITO_TOKEN"]
# --- Check the channel state ---
url = BASE_URL + "/channel"
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.get(url, headers=headers)
print(response.json())
# --- Request a pairing code ---
url = BASE_URL + "/channel/pairing-code"
payload = { "phone": "+15557654321" }
headers = {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
# --- Fall back to a QR if you need to ---
url = BASE_URL + "/channel/qr"
headers = {"Authorization": "Bearer " + TOKEN}
response = requests.get(url, headers=headers)
print(response.json())
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
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 poll loop with no attempt limit runs forever when the person never types the code; count iterations and stop with a clear message so the worker is not stuck.
- Printing the QR to a terminal with a library that renders it as ASCII works locally and fails on a server with no TTY; stream it to a browser instead.
- Catching Exception around the state read hides the 403 channel_locked that means the plan gate is on, and the loop polls a channel that will never connect; catch requests.HTTPError and inspect the code.
Frequently asked questions
Should I use a pairing code or a QR?
A pairing code, in almost every case. It can be read aloud, pasted into a chat or typed from a support ticket, and it does not depend on a camera pointed at a refreshing image. QR linking of new devices has also been unreliable across engine libraries since mid-2026, so treat it as the fallback.
Can I keep using WhatsApp on the phone afterwards?
Yes - that is the central difference from the official platform. Wapito links a companion device, exactly like WhatsApp Web, so the human keeps their app, their chats and their history while your automation works alongside them on the same number.
What happens if the session drops?
The channel webhook fires with the new state, the previous state and a reason. A transient engine restart reconnects by itself; a logout by the phone's owner or a ban does not, and both need a person. Branch on the reason rather than retrying blindly into a session that is gone.
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.