Índice
Primeiros Passos
Connect & Send
Receive & Automate
WhatsApp API with Python: Send, Receive and Automate Messages
Everything you need to use the WhatsApp API in Python: which library to pick, how to connect a linked account with a QR code, how to send and receive messages with
solicitações e httpx, and how to wire a FastAPI webhook for a bot or an AI agent. REST API today, no Meta Business verification needed
import requests
BASE_URL = "https://{YOUR_DSN}/api/v1"
HEADERS = {"X-API-KEY": "YOUR_ACCESS_TOKEN"}
def send_whatsapp(chat_id: str, text: str) -> dict:
response = requests.post(
f"{BASE_URL}/chats/{chat_id}/messages",
headers=HEADERS,
data={"text": text},
)
response.raise_for_status()
return response.json()
send_whatsapp("9f9uio56sopa456s", "Hello from Python!")200 OK, message sent
Definição
What is the WhatsApp API in Python?
O WhatsApp API in Python means calling WhatsApp Business messaging from Python code instead of clicking through WhatsApp Web. A Python script or backend service sends an HTTP request, usually with
solicitações ou httpx, to a REST API that exposes chats, messages and webhooks, either Meta's Cloud API directly or a unified provider like Unipile that connects WhatsApp alongside LinkedIn, Instagram and Telegram behind one interface. Meta does not publish an official all-in-one WhatsApp Python SDK, which is why almost every Python integration in this guide, and most of what you will find on the rest of the web, talks to the REST endpoints directly.One REST API for WhatsApp, LinkedIn, Instagram and Telegram
Linked with a QR code or a pairing code, no Meta Business verification
solicitações for simple scripts, httpx for async at scale For the orientation layer, access, cost and limits, start with the WhatsApp API access, cost and limits guide. Building in PHP instead of Python?
Read the same integration in PHP Before you build
What do you need before you start?
You do not need a Meta Business account or a WhatsApp Business Platform approval to start sending and receiving messages from Python. You need a working Python environment, an HTTP client, a place to receive webhooks, and a Unipile account.
Python 3.9+
Matches the minimum version required by the Unipile Python SDK's own
pydantic dependency, and by modern httpx and FastAPI releases.requests or httpx
pip install requests for synchronous calls, or pip install httpx if you plan to send messages concurrently. See the solicitações e httpx documentação.A local tunnel
A tool like
ngrok ou cloudflared to expose your FastAPI webhook route on a public HTTPS URL while you develop.Access Token and DSN
Both come from the Unipile dashboard. The DSN is the host you call, the Access Token goes in the
X-API-KEY header of every request.Comparação de Bibliotecas
Which Python library should you use for WhatsApp?
There is no single, obvious answer, and most blog posts on this topic push whichever package their author maintains. Here is a factual comparison of the five ways Python developers actually call WhatsApp today, from browser automation to a unified provider, so you can pick based on what each option really does, not on marketing.
Library / approach
What it actually calls
Meta Business verification
Maintained
Melhor para
pywhatkit
Drives WhatsApp Web in a browser tab (keyboard automation), not a server-side API
N/A, no API
Community, sporadic
One-off scripts and demos on a machine with a screen, not production sending
whatsapp-cloud-api
Thin Python wrapper around Meta's own WhatsApp Cloud API endpoints
Obrigatório
Community wrapper
Teams already approved on the Cloud API who want a Pythonic client
whatsapp-api-client-python
Calls the green-api SaaS, which in turn holds the WhatsApp session
Não é necessário
Vendor-maintained
Teams comfortable depending on a second, single-purpose SaaS vendor
requests, direct to Meta
Meta's Cloud API endpoints, no wrapper at all
Obrigatório
You own it (DIY)
Teams already cleared for Meta Business verification who want full control
Unipile, requests or httpx
Unipile's unified messaging API, WhatsApp alongside LinkedIn, Instagram and Telegram
Não é necessário
Actively maintained, Python SDK in beta
SaaS products connecting many end-user WhatsApp accounts on behalf of each user
pywhatkit
ChamadasDrives WhatsApp Web in a browser tab, not a server-side API
Meta verificationN/A, no API
MaintainedCommunity, sporadic
Melhor paraOne-off scripts and demos, not production sending
whatsapp-cloud-api
ChamadasThin wrapper around Meta's own Cloud API endpoints
Meta verificationObrigatório
MaintainedCommunity wrapper
Melhor paraTeams already approved on the Cloud API
whatsapp-api-client-python
ChamadasThe green-api SaaS, which holds the WhatsApp session
Meta verificationNão é necessário
MaintainedVendor-maintained
Melhor paraTeams comfortable with a second SaaS vendor
requests, direct to Meta
ChamadasMeta's Cloud API endpoints, no wrapper
Meta verificationObrigatório
MaintainedYou own it (DIY)
Melhor paraTeams already cleared for Meta Business verification
Unipile, requests or httpx
ChamadasUnified messaging API, WhatsApp with LinkedIn, Instagram, Telegram
Meta verificationNão é necessário
MaintainedActively maintained, Python SDK in beta
Melhor paraSaaS products connecting many end-user accounts
Every option that keeps Meta Business verification required ultimately talks to Meta's own WhatsApp Cloud API (see Meta's WhatsApp Business Platform documentation), which is the right call if you already run a verified Business number. If you are connecting WhatsApp accounts on behalf of many different end users instead, a QR code or pairing code flow removes that verification step entirely, which is the model the rest of this guide uses.
Conexão de conta
How do you connect a WhatsApp account in Python?
Every call in this guide runs em nome de um usuário autenticado who linked their own WhatsApp account. There is no Meta Business verification step: from Python, you send a single
POST /api/v1/accounts request with provider set to WHATSAPP, and the user confirms the link either by scanning a QR code or by typing a pairing code into their phone.Option A: QR code (default)
Leave
pairing_phone_number out of the request body and Unipile returns a checkpoint carrying the QR code payload. Render it with a library such as qrcode and display it for the user to scan from WhatsApp > Linked Devices.import requests
BASE_URL = "https://{YOUR_DSN}/api/v1"
HEADERS = {"X-API-KEY": "YOUR_ACCESS_TOKEN", "accept": "application/json"}
def connect_whatsapp_qr() -> dict:
response = requests.post(
f"{BASE_URL}/accounts",
headers=HEADERS,
json={"provider": "WHATSAPP"},
)
response.raise_for_status()
return response.json()
account = connect_whatsapp_qr()
print(account)
# account holds a new account_id plus a checkpoint object.
# Render the checkpoint as a QR code and have the user scan it
# from WhatsApp > Linked Devices.Option B: pairing code
Passar
pairing_phone_number in E.164 digits only, country code first, no plus sign, no spaces. Unipile returns a checkpoint carrying a short code the user types into WhatsApp instead of scanning anything, which is a better fit for a headless server with no screen to show a QR code on.def connect_whatsapp_pairing_code(phone_e164_digits: str) -> dict:
response = requests.post(
f"{BASE_URL}/accounts",
headers=HEADERS,
json={
"provider": "WHATSAPP",
"pairing_phone_number": phone_e164_digits,
},
)
response.raise_for_status()
return response.json()
# Country code + number, digits only, e.g. France
account = connect_whatsapp_pairing_code("33612345678")
# account's checkpoint carries the pairing code to show in your UI.
# The user enters it in WhatsApp > Linked Devices > Link with phone number.Confirm that the account is connected
Once the user scans the QR code or types the pairing code, you need to know when the account is actually ready to send and receive. Unipile gives you two options.
Poll the account status
Chamar
GET /api/v1/accounts/{account_id} every couple of seconds until the account no longer needs its checkpoint. Simple, but it wastes requests while you wait.Account Status Webhook
Register a webhook with
source set to status_da_conta (same POST /api/v1/webhooks endpoint used for messages) and Unipile pushes {"AccountStatus": {"account_id": "...", "message": "OK"}} the instant the account is ready. This is what most production integrations use.import time
def wait_for_connection(account_id: str, timeout_seconds: int = 120) -> dict:
deadline = time.time() + timeout_seconds
while time.time() < deadline:
response = requests.get(f"{BASE_URL}/accounts/{account_id}", headers=HEADERS)
response.raise_for_status()
account = response.json()
print(account) # inspect the live payload for your own status check
time.sleep(2)
raise TimeoutError("WhatsApp account did not confirm connection in time")Enviar
How do you send a WhatsApp message with Python?
Sending a WhatsApp message from Python is a single HTTP call:
POST /api/v1/chats/{chat_id}/messages, with the chat_id you already have from listing chats or from an incoming webhook, and a text field. The endpoint accepts form-encoded and JSON-encoded bodies alike, so a plain requests.post com dados= works with no extra setup.If there is no existing
chat_id, for example the first message to a new contact, call POST /api/v1/chats instead with an account_id and the recipient's attendees_ids; Unipile creates the 1:1 chat and sends the message in the same request.import requests
BASE_URL = "https://{YOUR_DSN}/api/v1"
HEADERS = {"X-API-KEY": "YOUR_ACCESS_TOKEN"}
def send_message(chat_id: str, text: str) -> dict:
response = requests.post(
f"{BASE_URL}/chats/{chat_id}/messages",
headers=HEADERS,
data={"text": text},
)
response.raise_for_status()
return response.json()
def start_chat(account_id: str, attendee_provider_id: str, text: str) -> dict:
response = requests.post(
f"{BASE_URL}/chats",
headers=HEADERS,
data={
"account_id": account_id,
"attendees_ids": attendee_provider_id,
"text": text,
},
)
response.raise_for_status()
return response.json()Message types (text, media, voice notes, templates) and the 24-hour customer service window are their own topic, covered in full in the guide to message types the WhatsApp API supports. This section only covers the Python plumbing.
Meta bills WhatsApp API messages per message sent, not per conversation, since July 2025. See how WhatsApp API pricing works per message for current per-country rates.
Concurrency
How do you send WhatsApp messages asynchronously in Python?
When you send to many recipients at once, a broadcast, a queue drain, a bulk follow-up job, a loop that awaits one
requests.post at a time is the bottleneck, not the API. httpx.AsyncClient combined with asyncio.gather fires many requests concurrently from a single event loop, no threads required. A semaphore is what keeps you a good citizen: WhatsApp accounts are still bound by the platform's own rate limits, so uncapped concurrency just trades a slow loop for a wall of 429 responses.import asyncio
import httpx
BASE_URL = "https://{YOUR_DSN}/api/v1"
HEADERS = {"X-API-KEY": "YOUR_ACCESS_TOKEN"}
CONCURRENCY_LIMIT = 5
async def send_message_async(
client: httpx.AsyncClient,
semaphore: asyncio.Semaphore,
chat_id: str,
text: str,
) -> dict:
async with semaphore:
response = await client.post(
f"{BASE_URL}/chats/{chat_id}/messages",
headers=HEADERS,
data={"text": text},
)
response.raise_for_status()
return response.json()
async def send_bulk(messages: list[tuple[str, str]]) -> list:
semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT)
async with httpx.AsyncClient(timeout=30) as client:
tasks = [
send_message_async(client, semaphore, chat_id, text)
for chat_id, text in messages
]
return await asyncio.gather(*tasks, return_exceptions=True)
messages = [
("9f9uio56sopa456s", "Your order shipped!"),
("a1b2c3d4e5f6g7h8", "Your order shipped!"),
]
results = asyncio.run(send_bulk(messages))
for chat_id_text, result in zip(messages, results):
if isinstance(result, Exception):
print("failed:", chat_id_text[0], result)Keep
CONCURRENCY_LIMIT conservative and raise it only after you have watched real error rates. return_exceptions=True means one failed send does not cancel the rest of the batch, which matters once you are firing hundreds of messages in one run. The rate limit and retry section below builds on this same pattern.Webhooks
How do you receive WhatsApp messages with a FastAPI webhook?
A webhook is how your Python service learns about a new WhatsApp message the moment it arrives, instead of polling for it. You register one endpoint with Unipile, and every matching event is delivered to your server as JSON. FastAPI is a natural fit for the receiving side: a
pydantic model validates the payload for you, and BackgroundTasks lets your route return a response immediately while the real work, calling a model, writing to a database, happens after.1. Register the webhook
Point
request_url at your FastAPI route. During local development, that means the HTTPS URL your tunnel (from the prerequisites section above) exposes, not localhost.import requests
BASE_URL = "https://{YOUR_DSN}/api/v1"
HEADERS = {"X-API-KEY": "YOUR_ACCESS_TOKEN"}
response = requests.post(
f"{BASE_URL}/webhooks",
headers=HEADERS,
json={
"source": "messaging",
"request_url": "https://your-tunnel.example.com/webhooks/whatsapp",
"name": "whatsapp-fastapi",
"format": "json",
"events": ["message_received"],
},
)
response.raise_for_status()
print(response.json())2. Receive it in FastAPI
The payload Unipile sends carries the chat, the sender and the message text as flat fields, plus a list of attachments when there are any. Model only what you need; Pydantic ignores extra fields by default.
from typing import List, Optional
from fastapi import BackgroundTasks, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Sender(BaseModel):
attendee_id: str
attendee_name: Optional[str] = None
attendee_provider_id: str
class Attachment(BaseModel):
id: str
type: str
mimetype: Optional[str] = None
unavailable: bool = False
class WhatsAppMessageEvent(BaseModel):
account_id: str
account_type: str
event: str
chat_id: str
message_id: str
message: Optional[str] = None
timestamp: str
webhook_name: Optional[str] = None
sender: Sender
attachments: List[Attachment] = []
def handle_message(event: WhatsAppMessageEvent) -> None:
if event.account_type != "WHATSAPP" or event.event != "message_received":
return
# Unipile includes messages the linked account itself sent, from
# another device or from your own API calls. Compare the sender
# against the account owner you stored at connection time if you
# only want to react to messages coming from the other side.
print(f"New WhatsApp message on chat {event.chat_id}: {event.message}")
# forward to your queue, your database, or an AI agent from here
@app.post("/webhooks/whatsapp")
async def whatsapp_webhook(
event: WhatsAppMessageEvent,
background_tasks: BackgroundTasks,
):
background_tasks.add_task(handle_message, event)
return {"status": "received"}Run it with
uvicorn main:app --reload, point your tunnel at port 8000, and register that public URL as request_url in step 1. Returning {"status": "received"} before the message is fully processed matters: Unipile expects a fast response, and BackgroundTasks is what keeps handle_message from blocking it.One more thing worth building in from the start: store each processed
message_id before you act on it, and skip anything you have already seen. If the linked WhatsApp account itself disconnects and reconnects, Unipile delivers the messages that arrived during that gap once it catches up, and a redeploy or a crashed background task can separately cause your own handler to see the same event twice, so treating message_id as an idempotency key keeps a WhatsApp bot from double-replying.Ler
How do you retrieve chats and message history in Python?
Every list endpoint in the Unipile API, chats, messages, attendees, is paginated the same way: the response carries an
itens array and a cursor. Pass that cursor back on the next call, and stop once it comes back nulo. Python's while True loop maps onto that pattern directly.List every WhatsApp chat for an account
import requests
BASE_URL = "https://{YOUR_DSN}/api/v1"
HEADERS = {"X-API-KEY": "YOUR_ACCESS_TOKEN"}
def list_all_whatsapp_chats(account_id: str) -> list:
chats = []
cursor = None
while True:
params = {"account_id": account_id, "limit": 100}
if cursor:
params["cursor"] = cursor
response = requests.get(f"{BASE_URL}/chats", headers=HEADERS, params=params)
response.raise_for_status()
page = response.json()
chats.extend(page["items"])
cursor = page["cursor"]
if not cursor:
break
return chats
for chat in list_all_whatsapp_chats("Yk08cDzzdsqs9_8ds"):
print(chat["id"], chat["name"], chat["unread_count"])Read the message history and the attendees of one chat
Most recent messages come back first. Use the same
limite e cursor pattern to page backward through older history, and call the attendees endpoint whenever you need to resolve who is actually in a chat, individual or group.def get_chat_history(chat_id: str, limit: int = 100) -> list:
response = requests.get(
f"{BASE_URL}/chats/{chat_id}/messages",
headers=HEADERS,
params={"limit": limit},
)
response.raise_for_status()
return response.json()["items"]
def get_chat_attendees(chat_id: str) -> list:
response = requests.get(
f"{BASE_URL}/chats/{chat_id}/attendees",
headers=HEADERS,
)
response.raise_for_status()
return response.json()["items"]
messages = get_chat_history("9f9uio56sopa456s")
attendees = get_chat_attendees("9f9uio56sopa456s")This is also the code path a CRM sync or a support inbox actually runs in Python: on a fresh connection, walk every chat once with
list_all_whatsapp_chats to seed your database, then rely on the webhook from the previous section to keep it current instead of re-polling. The GET /chats endpoint also accepts an unread filter, so a lighter job that only checks for unread chats on a schedule does not have to page through a full account history each time it runs.WhatsApp groups are chats too, so the same
GET /chats e GET /chats/{chat_id}/attendees calls list them and their members from Python with no extra code. To add or remove a participant, or to fetch the invite link, use PATCH /chats/{chat_id} with an addParticipant, removeParticipantou getInviteLink action. The full walkthrough, together with Meta's own participant limits on its native Groups API, lives in the guide to add or remove WhatsApp group participants.Automação
How do you build a WhatsApp bot or AI agent in Python?
Every WhatsApp bot or AI agent follows the same three steps: receive a message through the webhook, decide what to do with it, send a reply on the same
chat_id. Nothing about steps one and three changes when you put an LLM in the middle, which is why the FastAPI handler and the enviar_mensagem function from the earlier sections are already most of the code you need.from openai import OpenAI # or any LLM client you use
llm = OpenAI()
_recent_bot_replies: dict = {}
def generate_reply(incoming_text: str) -> str:
completion = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful WhatsApp support agent."},
{"role": "user", "content": incoming_text},
],
)
return completion.choices[0].message.content
def handle_message(event: WhatsAppMessageEvent) -> None:
if event.account_type != "WHATSAPP" or event.event != "message_received":
return
if not event.message:
return
if _recent_bot_replies.get(event.chat_id) == event.message:
return # echo of our own last reply, ignore it
reply_text = generate_reply(event.message)
_recent_bot_replies[event.chat_id] = reply_text
send_message(event.chat_id, reply_text)O
_recent_bot_replies guard matters more than it looks: Unipile's message_received event fires for messages the linked account sends too, from another device or from your own API calls, so without a guard an agent can end up replying to its own reply. A dictionary is fine for a demo; a production agent should keep that state in Redis or a database alongside conversation history and an idempotency key.The webhook payload and the
enviar_mensagem call are shaped the same way across WhatsApp, LinkedIn, Instagram and Telegram, only account_type changes, so the same FastAPI handler can route one agent's replies across every channel a user connected. Keeping channel-specific tone, formatting and rate limits straight when you do that is covered in the guide to the API multicanal para agentes de IA.Confiabilidade
How do you handle rate limits and retries in Python?
Unipile returns a JSON body with a
tipo field, like errors/invalid_credentials ou errors/disconnected_account, whenever a request fails. Some of those are worth retrying, a dropped connection or a temporary provider hiccup; others, like bad credentials, will not fix themselves no matter how many times you call again. How fast and how much you send from a single WhatsApp account stays a customer-side decision, shaped by the rate limits WhatsApp itself enforces on that account, not a fixed number Unipile imposes on top.O
tenacidade package turns that into a decorator instead of a hand-rolled loop. Wrap the same enviar_mensagem function from the sending section with exponential backoff and a capped attempt count:# pip install tenacity
import requests
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=2, max=30),
retry=retry_if_exception_type(requests.exceptions.HTTPError),
)
def send_message_with_retry(chat_id: str, text: str) -> dict:
response = requests.post(
f"{BASE_URL}/chats/{chat_id}/messages",
headers=HEADERS,
data={"text": text},
)
response.raise_for_status()
return response.json()Five attempts with a 2 to 30 second exponential backoff is a sane default for a background job; drop the attempt count for anything running in a user-facing request path, since
tenacidade will otherwise hold the request open while it retries. The same decorator wraps the async version from the concurrency section, just add await onde tenacidade supports it through AsyncRetrying.SDK
Is there an official Unipile Python SDK?
Sim.
unipile/unipile-python is real and actively maintained, with a commit pushed as recently as August 11, 2026. Two things are worth knowing before you reach for it: it is not published on PyPI yet, and it targets Unipile's API v2, which is still in beta, while every endpoint used throughout this guide is v1.Actively maintained, GitHub only
Python 3.9+, pydantic 2.11+
Not on PyPI, install from GitHub
Targets API v2 beta, not v1
pip install git+https://github.com/unipile/unipile-python.gitimport unipile
configuration = unipile.Configuration()
configuration.api_key["apiKey"] = "YOUR_ACCESS_TOKEN"
api_client = unipile.ApiClient(configuration)
messaging_api = unipile.MessagingApi(api_client)For everything this guide covers today, build on the v1 REST endpoints with
solicitações ou httpx: they are stable, documented, and what every code sample above actually runs on. Once the Python SDK graduates out of beta and lands on PyPI, migrating mostly means swapping HTTP calls for typed client methods, the endpoints and the connected-account model underneath do not change. For comparison, the Node.js SDK is the one currently referenced throughout Unipile's main documentation index, and the older PHP SDK has been archived since October 2023.Building the same kind of integration for another channel? The Instagram API with Python guide follows the identical connect, send, retrieve and webhook pattern shown in this article.
WhatsApp API in Python: FAQ
Straight answers on libraries, connection, SDKs and webhooks for building the WhatsApp API in Python.
There is no single best library, it depends on what you already have. If you are already approved on Meta's Cloud API,
whatsapp-cloud-api gives you a Pythonic wrapper around it. If you want to skip Meta Business verification entirely and connect WhatsApp accounts on behalf of your users instead, Unipile's REST API called with solicitações ou httpx covers WhatsApp alongside LinkedIn, Instagram and Telegram from one interface. pywhatkit is worth ruling out early: it drives WhatsApp Web in a browser, not a server-side API, so it does not fit a production backend.Send a
POST request to /api/v1/chats/{chat_id}/mensagens with a text field and your X-API-KEY header, for example requests.post(url, headers=headers, data={"text": "Hello"}). If you do not have a chat_id yet, POST /api/v1/chats with an account_id and the recipient's attendees_ids creates the chat and sends the first message in the same call.Yes. Connecting a WhatsApp account through Unipile only requires the account owner to scan a QR code or enter a pairing code, the same way WhatsApp Web works. There is no Meta Business Platform application and no phone number verification with Meta, because the integration runs em nome do usuário autenticado rather than through a registered WhatsApp Business number.
Sim,
unipile/unipile-python is an actively maintained official SDK, but it is not on PyPI yet: install it with pip install git+https://github.com/unipile/unipile-python.git. It also targets Unipile's API v2, which is in beta, while every endpoint in this guide uses the stable v1 API, so most Python integrations today are still built directly on solicitações ou httpx rather than the SDK.solicitações is simpler and enough for scripts, cron jobs and low-volume sending. httpx is worth the switch once you are sending to many recipients at once or building an async FastAPI service, because its AsyncClient lets you fire multiple requests concurrently from the same event loop instead of blocking on each one.Run a FastAPI app locally, expose it with a tunnel like
ngrok ou cloudflared to get a public HTTPS URL, and register that URL as request_url when you create a webhook with source set to messaging. Unipile posts every new message to that tunnel URL, which forwards it straight to your local FastAPI route while you develop.Yes. WhatsApp groups show up as regular chats, so
GET /chats e GET /chats/{chat_id}/attendees list them and their members with no special-casing. To add a participant, remove one, or fetch the group invite link, send a PATCH /chats/{chat_id} request with an addParticipant, removeParticipantou getInviteLink action, detailed in the guide to adding and removing WhatsApp group members.The underlying endpoints are identical, the difference is the runtime and its ecosystem: Python favors
solicitações ou httpx com asyncio for concurrency and FastAPI for webhooks, while PHP integrations typically use Guzzle and fit into a Laravel application with queued jobs. If your stack is PHP, the same integration in PHP walks through every equivalent step.Ainda tem dúvidas? Nossa equipe está aqui para ajudar.