WhatsApp API with Python: Send, Receive and Automate Messages

WhatsApp API in PythonPython + WhatsApp API

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 żądania oraz httpx, and how to wire a FastAPI webhook for a bot or an AI agent.
REST API today, no Meta Business verification needed
send_whatsapp.py
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
Definicja

What is the WhatsApp API in Python?

The 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 żądania lub 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
żądania 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 żądania oraz httpx dokumentacja.
A local tunnel
A tool like ngrok lub 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.
Porównanie bibliotek

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
Najlepszy dla
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
Wymagany
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
Niewymagane
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
Wymagany
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
Niewymagane
Actively maintained, Python SDK in beta
SaaS products connecting many end-user WhatsApp accounts on behalf of each user
pywhatkit
PołączeniaDrives WhatsApp Web in a browser tab, not a server-side API
Meta verificationN/A, no API
MaintainedCommunity, sporadic
Najlepszy dlaOne-off scripts and demos, not production sending
whatsapp-cloud-api
PołączeniaThin wrapper around Meta's own Cloud API endpoints
Meta verificationWymagany
MaintainedCommunity wrapper
Najlepszy dlaTeams already approved on the Cloud API
whatsapp-api-client-python
PołączeniaThe green-api SaaS, which holds the WhatsApp session
Meta verificationNiewymagane
MaintainedVendor-maintained
Najlepszy dlaTeams comfortable with a second SaaS vendor
requests, direct to Meta
PołączeniaMeta's Cloud API endpoints, no wrapper
Meta verificationWymagany
MaintainedYou own it (DIY)
Najlepszy dlaTeams already cleared for Meta Business verification
Unipile, requests or httpx
PołączeniaUnified messaging API, WhatsApp with LinkedIn, Instagram, Telegram
Meta verificationNiewymagane
MaintainedActively maintained, Python SDK in beta
Najlepszy dlaSaaS 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.
Połączenie z kontem

How do you connect a WhatsApp account in Python?

Every call in this guide runs w imieniu uwierzytelnionego użytkownika 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.
connect_qr.py
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
Podanie 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.
connect_pairing_code.py
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
Zadzwoń 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_konta (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.
poll_status.py
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")
Wyślij

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 z dane= 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.
send.py
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.
send_bulk_async.py
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.
register_webhook.py
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.
main.py
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.
Czytaj

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 Przedmioty array and a kursor. Pass that kursor back on the next call, and stop once it comes back null. Python's while True loop maps onto that pattern directly.
List every WhatsApp chat for an account
list_chats.py
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 limit oraz kursor 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.
chat_history.py
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 oraz 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, removeParticipantlub 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.
Automatyzacja

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 wyślij_wiadomość function from the earlier sections are already most of the code you need.
agent.py
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)
The _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 wyślij_wiadomość 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 Wielokanałowe API dla agentów AI.
Niezawodność

How do you handle rate limits and retries in Python?

Unipile returns a JSON body with a typ field, like errors/invalid_credentials lub 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.
The wytrwałość package turns that into a decorator instead of a hand-rolled loop. Wrap the same wyślij_wiadomość function from the sending section with exponential backoff and a capped attempt count:
retry.py
# 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 wytrwałość will otherwise hold the request open while it retries. The same decorator wraps the async version from the concurrency section, just add czekać gdzie wytrwałość supports it through AsyncRetrying.
SDK

Is there an official Unipile Python SDK?

Tak. 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
install_sdk.sh
pip install git+https://github.com/unipile/unipile-python.git
sdk_usage.py
import 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 żądania lub 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 żądania lub 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/czaty/{chat_id}/wiadomości 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 w imieniu uwierzytelnionego użytkownika rather than through a registered WhatsApp Business number.
Tak, 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 żądania lub httpx rather than the SDK.
żądania 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 lub cloudflared to get a public HTTPS URL, and register that URL as request_url when you create a webhook with source set to przekazywanie wiadomości. 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 oraz 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, removeParticipantlub 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 żądania lub httpx z 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.

Masz jeszcze jakieś pytania? Nasz zespół służy pomocą.

Porozmawiaj z ekspertem
pl_PLPL