Blog/Tutorials

How to Send WhatsApp Messages with Python: Full Code Tutorial (2026)

Send WhatsApp messages with Python using the Rapiwa REST API. Complete tutorial with requests library, async aiohttp, Django, Flask, and error handling examples.

by Saikat
How to Send WhatsApp Messages with Python: Full Code Tutorial (2026)

You can send WhatsApp messages with Python by making an HTTP POST request to the Rapiwa API using the requests library. With Rapiwa ($5/month), connect your WhatsApp number, get an API key, and send messages in 3 lines of Python. This tutorial covers basic sending, bulk messaging, file attachments, error handling, async sending, and Django/Flask integration.

Prerequisites

pip install requests
  • Rapiwa account: free 3-day trial
  • Your API key from Dashboard → API Keys
  • WhatsApp number connected via QR code

Step 1: Send Your First Message

import requests

API_KEY = "YOUR_RAPIWA_API_KEY"

response = requests.post(
    "https://app.rapiwa.com/send-message",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"number": "8801234567890", "message": "Hello from Python!"}
)

print(response.json())
# {'status': 'success', 'messageId': 'msg_abc123', 'timestamp': '...'}

Phone number format: international format, no +, no spaces. Bangladesh: 8801234567890.

Step 2: Create a Reusable WhatsApp Client

import requests
from typing import Optional

class RapiwaClient:
    """WhatsApp messaging client using Rapiwa API."""
    
    BASE_URL = "https://app.rapiwa.com"
    
    def __init__(self, api_key: str):
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        })
    
    def send_text(self, phone: str, message: str) -> dict:
        """Send a text message."""
        response = self.session.post(
            f"{self.BASE_URL}/send-message",
            json={"number": phone, "message": message}
        )
        response.raise_for_status()
        return response.json()
    
    def send_image(self, phone: str, image_url: str, caption: str = "") -> dict:
        """Send an image with optional caption."""
        payload = {"number": phone, "image": image_url}
        if caption:
            payload["caption"] = caption
        response = self.session.post(f"{self.BASE_URL}/send-message", json=payload)
        response.raise_for_status()
        return response.json()
    
    def send_document(self, phone: str, doc_url: str, filename: str, caption: str = "") -> dict:
        """Send a document (PDF, DOCX, etc.)."""
        payload = {"number": phone, "document": doc_url, "filename": filename}
        if caption:
            payload["caption"] = caption
        response = self.session.post(f"{self.BASE_URL}/send-message", json=payload)
        response.raise_for_status()
        return response.json()
    
    def check_number(self, phone: str) -> dict:
        """Check if a phone number is registered on WhatsApp."""
        response = self.session.get(
            f"{self.BASE_URL}/check-number",
            params={"number": phone}
        )
        response.raise_for_status()
        return response.json()

# Usage
client = RapiwaClient("YOUR_API_KEY")
result = client.send_text("8801234567890", "Hello!")
print(result)

Step 3: Send Different Message Types

client = RapiwaClient("YOUR_API_KEY")

# Text with WhatsApp formatting
client.send_text("8801234567890", 
    "*Bold text*\n_Italic text_\n~Strikethrough~\n`Monospace`"
)

# Image with caption
client.send_image("8801234567890",
    "https://yourstore.com/product.jpg",
    caption="Check out our new product!"
)

# PDF invoice
client.send_document("8801234567890",
    "https://yourstore.com/invoices/INV-001.pdf",
    filename="Invoice_001.pdf",
    caption="Your invoice is attached."
)

# Location
requests.post("https://app.rapiwa.com/send-message",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "number": "8801234567890",
        "latitude": 23.8103,
        "longitude": 90.4125,
        "locationName": "Our Store — Dhaka"
    }
)

Step 4: Bulk Sending with Error Handling

import requests
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def send_bulk_messages(contacts: list[dict], message_template: str, 
                        api_key: str, delay: float = 1.5) -> dict:
    """
    Send personalized messages to multiple contacts.
    
    contacts: list of dicts with 'phone' and any template variables
    message_template: string with {variable} placeholders
    delay: seconds to wait between messages
    """
    results = {"success": [], "failed": []}
    
    for i, contact in enumerate(contacts, 1):
        try:
            message = message_template.format(**contact)
            response = requests.post(
                "https://app.rapiwa.com/send-message",
                headers={"Authorization": f"Bearer {api_key}"},
                json={"number": contact["phone"], "message": message},
                timeout=10
            )
            response.raise_for_status()
            data = response.json()
            
            if data.get("status") == "success":
                results["success"].append(contact["phone"])
                logger.info(f"[{i}/{len(contacts)}] ✓ {contact['phone']}")
            else:
                results["failed"].append({"phone": contact["phone"], "error": data})
                
        except requests.exceptions.RequestException as e:
            logger.error(f"[{i}/{len(contacts)}] ✗ {contact['phone']}: {e}")
            results["failed"].append({"phone": contact["phone"], "error": str(e)})
        
        if i < len(contacts):
            time.sleep(delay)
    
    logger.info(f"Done: {len(results['success'])} sent, {len(results['failed'])} failed")
    return results

# Usage
contacts = [
    {"phone": "8801111111111", "name": "Alice", "order_id": "12345"},
    {"phone": "8802222222222", "name": "Bob", "order_id": "12346"},
]

results = send_bulk_messages(
    contacts,
    message_template="Hi {name}! Your order #{order_id} is confirmed.",
    api_key="YOUR_API_KEY"
)

Step 5: Async Sending with aiohttp

For high-throughput applications, use async sending:

import asyncio
import aiohttp

API_KEY = "YOUR_API_KEY"

async def send_whatsapp_async(session: aiohttp.ClientSession, phone: str, message: str) -> dict:
    async with session.post(
        "https://app.rapiwa.com/send-message",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"number": phone, "message": message}
    ) as response:
        return await response.json()

async def send_bulk_async(contacts: list[dict], message: str, max_concurrent: int = 10):
    """Send messages concurrently with rate limiting."""
    semaphore = asyncio.Semaphore(max_concurrent)
    
    async def send_with_limit(contact):
        async with semaphore:
            result = await send_whatsapp_async(session, contact["phone"], 
                                                message.format(**contact))
            await asyncio.sleep(0.1)  # Small delay even in async
            return result
    
    async with aiohttp.ClientSession() as session:
        tasks = [send_with_limit(c) for c in contacts]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    
    return results

# Usage
contacts = [{"phone": f"880100000{i:04d}", "name": f"User{i}"} for i in range(100)]
results = asyncio.run(send_bulk_async(contacts, "Hi {name}! Your account is active."))

Install aiohttp: pip install aiohttp

Step 6: Django Integration

# yourapp/services/whatsapp.py
import requests
from django.conf import settings

def send_order_notification(customer_phone: str, order_id: int, total: str):
    """Send WhatsApp order notification using Django settings."""
    message = (
        f"*Order Confirmed!* 🛒\n\n"
        f"Order #{order_id}\n"
        f"Total: ${total}\n\n"
        f"We'll update you when it ships!"
    )
    
    response = requests.post(
        "https://app.rapiwa.com/send-message",
        headers={"Authorization": f"Bearer {settings.RAPIWA_API_KEY}"},
        json={"number": customer_phone, "message": message},
        timeout=10
    )
    
    if not response.ok:
        raise ValueError(f"WhatsApp API error: {response.text}")
    
    return response.json()

# In settings.py:
# RAPIWA_API_KEY = os.getenv("RAPIWA_API_KEY")

# In views.py:
# from .services.whatsapp import send_order_notification
# send_order_notification(order.phone, order.id, str(order.total))

Step 7: Flask Integration

from flask import Flask, request, jsonify
import requests
import os

app = Flask(__name__)
RAPIWA_KEY = os.getenv("RAPIWA_API_KEY")

def send_whatsapp(phone: str, message: str) -> bool:
    resp = requests.post(
        "https://app.rapiwa.com/send-message",
        headers={"Authorization": f"Bearer {RAPIWA_KEY}"},
        json={"number": phone, "message": message}
    )
    return resp.json().get("status") == "success"

@app.route("/notify-customer", methods=["POST"])
def notify_customer():
    data = request.json
    success = send_whatsapp(data["phone"], f"Hi {data['name']}! Your order is confirmed.")
    return jsonify({"sent": success})

if __name__ == "__main__":
    app.run(port=5000)

FAQ

What Python library should I use to send WhatsApp messages? Use the requests library for simplicity. For async applications, use aiohttp. For Django, the built-in django.utils.http or requests both work.

How do I send WhatsApp messages with attachments in Python? Use the document field with a publicly accessible URL: json={"number": phone, "document": url, "filename": "file.pdf"}.

Can I send WhatsApp messages from a Python script without a phone connected? Yes. Once your WhatsApp number is connected in the Rapiwa dashboard via QR code, your Python script can make API calls from any server without a phone present.

How do I handle rate limits in Python? Add time.sleep(1.5) between messages, or use exponential backoff when receiving 429 status codes. See the bulk sending example above.

What is the best Python SDK for Rapiwa? There is no official Rapiwa Python SDK yet (on the roadmap). The requests library with 3–5 lines of code is all you need for full functionality.