How to Send WhatsApp Messages via API (Python & curl)

Published August 23, 2026 Β· UpdatedOctober 9, 2026

Sometimes validation is not enough and you need to actually send a WhatsApp message: an onboarding text, a notification, a support reply or a transactional update. The WhatsApp Messaging API does this over plain HTTP. You link a WhatsApp account once, and from then on every call carries the resulting sessionId.

This guide walks through the whole path in curl and Python: create and link a session, send text and media, validate recipients, read replies, and keep the account in good standing. Use it only with accounts you own or are authorized to operate, and follow WhatsApp's Terms of Service.

How it works

Sending is session-based. A session represents one linked WhatsApp account, so the flow is:

  1. Create a session, which starts the login and gives you a session id.
  2. Link the account by scanning a QR code or typing an 8-character pairing code in the WhatsApp app.
  3. Poll the session until its status reads CONNECTED.
  4. Send messages by passing the sessionId to the send operations.

After connecting, the session id becomes a stable ID derived from the linked device, so you can reuse it across restarts without linking again.

Step 1: Get an API key

Sign up and copy an active key from your Dashboard. Free credits are included, so you can test the flow end to end.

How requests are shaped

The key goes in key and the session in sessionId. For GET and DELETE operations everything else is a query parameter. For POST operations, the JSON payload is sent as a string inside a body field. Every reply is wrapped twice, as {"code": 200, "message": "success", "data": {"code": 200, "data": {...}}}: the outer layer comes from the platform and the inner one from the messaging service, which always answers with HTTP 200 and reports a failure in its own code, title and detail. This small Python helper hides the body string and both layers, raises on any failure and is used in every step below:

import json
import time
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.opendata-api.com/api/project/whatsapp-messaging"
SUCCESS_CODES = {200, 201, 202, 204}


def call(method, operation, payload=None, **params):
    """Call one operation and return the innermost payload (dict, list or None)."""
    params["key"] = API_KEY
    kwargs = {"params": params, "timeout": 60}
    if payload is not None:
        kwargs["json"] = {"body": json.dumps(payload)}
    resp = requests.request(method, f"{BASE}/{operation}", **kwargs)
    if not resp.ok:
        raise RuntimeError(f"{operation} failed: HTTP {resp.status_code} {resp.text}")
    body = resp.json()
    # Peel both envelopes: {"code": 200, "data": {"code": 201, "data": {...}}}
    while isinstance(body, dict) and "code" in body:
        if body["code"] not in SUCCESS_CODES:
            raise RuntimeError(f"{operation} failed: {body}")
        body = body.get("data")
    return body

create-session supports two login modes. QR login is the recommended one:

curl -X POST "https://api.opendata-api.com/api/project/whatsapp-messaging/create-session?key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"{\"mode\":\"qr\",\"deviceName\":\"My Bot\"}"}'

Then poll get-session with the session ID. While login is pending, its response carries a scannable qrImage (a base64 data-URI) and a qrPageUrl you can open in a browser to watch the login live. On the phone, open WhatsApp, go to Settings, then Linked Devices, choose Link a device and scan the code.

If scanning is awkward, use pairing-code login instead. Send {"mode":"pairing","phoneNumber":"8613800138000","deviceName":"My Bot"}, where phoneNumber is the full international number, digits only, with no + and no spaces. The status response then returns an 8-character pairingCode. On the phone choose Link a device, then Link with phone number instead, and type the code. deviceName is the label shown under Linked Devices.

The create-session reply carries the session in data.data with an inner code of 201. Its identifier is the id field, which every following call passes as the sessionId query parameter.

session = call("POST", "create-session", {"mode": "qr", "deviceName": "My Bot"})
# The identifier is the "id" field; "sessionId" is only the query-parameter name.
session_id = (session or {}).get("id")
if not session_id:
    raise RuntimeError(f"no session id in the response: {session}")

deadline = time.time() + 180
while time.time() < deadline:
    state = call("GET", "get-session", sessionId=session_id)
    print(state.get("status"), state.get("qrPageUrl"))
    if state.get("status") == "CONNECTED":
        break
    if state.get("status") in ("FAILED", "LOGGED_OUT"):
        raise RuntimeError(f"login failed: {state}")
    time.sleep(3)
else:
    raise TimeoutError("account was not linked in time")

The statuses are CREATED, WAITING_FOR_SCAN, CONNECTING, CONNECTED, DISCONNECTED, LOGGED_OUT and FAILED.

Step 3: Send a text message

Once the session is CONNECTED, call send-message with the recipient in to and the message in text. One detail trips up almost everyone: to is the full international number, digits only with no +, and it must be a JSON number, not a string.

curl -X POST "https://api.opendata-api.com/api/project/whatsapp-messaging/send-message?key=YOUR_API_KEY&sessionId=SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"body":"{\"to\":8613800138000,\"text\":\"Hello from the API!\"}"}'
sent = call("POST", "send-message", {"to": 8613800138000, "text": "Hello from the API!"}, sessionId=session_id)
print(sent)   # id, timestamp and to

The reply holds the message id, a timestamp and to. To send a quoted reply, add quoteMessageId with a message id taken from the inbox or chat history.

Step 4: Validate recipients first

Sending to a number that is not on WhatsApp wastes a call and looks bad on an account. check-contact-exists uses your connected session to tell you whether a number has an active WhatsApp account. The reply carries exists, the contact's jid and the query you sent. The number goes in as phoneNumber, again digits only.

info = call("GET", "check-contact-exists", sessionId=session_id, phoneNumber="8613800138000")
print(info.get("exists"), info.get("jid"))

For lists, the session-free lookups are a better fit: see how to check if a number is on WhatsApp, the WhatsApp number lookup guide and bulk verification of WhatsApp numbers.

Step 5: Send media

send-media takes the recipient as to (a JSON number again), a type of image, video, audio, voice, document or sticker, and the base64-encoded file as data. voice sends a push-to-talk voice note. A document requires fileName; caption adds a caption, mimeType overrides the inferred type and seconds sets the audio or video duration.

import base64

with open("receipt.pdf", "rb") as fh:
    data = base64.b64encode(fh.read()).decode("ascii")

call("POST", "send-media", {
    "to": 8613800138000,
    "type": "document",
    "data": data,
    "fileName": "receipt.pdf",
    "caption": "Your receipt",
}, sessionId=session_id)

Step 6: Read replies

list-inbox returns messages received in real time while the session was connected, newest first. Each item has the message id, chatJid, sender, type, text and timestamp. The inbox holds the most recent 500 messages per session and is cleared on restart, so poll it and copy what you need into your own database. Use limit to cap the page, and mark-read with the chatJid and messageIds to send read receipts.

messages = call("GET", "list-inbox", sessionId=session_id, limit=50) or []
for msg in messages:
    print(msg["id"], msg["chatJid"], msg.get("text"))

Error handling and sessions

  • Read the inner code, not just the HTTP status. The messaging service answers with HTTP 200 even when it rejects a request, and reports the outcome in the inner code with a title and detail; call() raises on it. A platform-level 401 means the API key is missing or invalid, and 403 means insufficient credits or an inactive account.
  • Check the session before retrying. If sends start failing, call get-session. A saved account that is only offline is reconnected by that call.
  • Do not blindly retry sends. There is no idempotency key. After a timeout, look at the chat history before sending again.
  • Disconnect deliberately. disconnect-session drops the live connection but keeps the saved login. Add logout=true to log out of WhatsApp completely; the account then has to be linked again.

Use it responsibly

Unsolicited and bulk messaging goes against WhatsApp's terms, and an account that sends unwanted messages risks being restricted. To stay in good standing:

  • Message people who expect it: opted-in users, customers and transactional recipients.
  • Do not blast cold lists. Validate numbers first, and contact only reachable, consented people.
  • Pace your sends with delays, and avoid sending identical text to many recipients.
  • Respect WhatsApp's terms and the messaging and privacy laws that apply to your use case.

If your audience also lives on Telegram, the same pattern is covered in sending Telegram messages from Python and the Telegram login guide. To decide which channel each contact should get, follow the lead verification workflow.

FAQ

How do I send a WhatsApp message through an API? Create a session with create-session, link the account by scanning the QR code or entering the pairing code, wait until get-session reports CONNECTED, then call send-message with the sessionId, the recipient number as to and your text.

Why does send-message reject my number? The to field must be the full international number, digits only with no plus sign, and it must be sent as a JSON number rather than a string. For example 8613800138000, not "+86 138 0013 8000".

Do I have to scan the QR code every time? No. After the first link, the session id becomes a stable ID derived from the device and survives restarts. Call get-session or connect-session to bring a saved but offline account back. logout=true deletes the saved credentials, so you link again.

Can I get incoming WhatsApp messages? Yes. list-inbox returns the messages received while the session was connected, newest first, up to the latest 500 per session. The inbox is cleared on restart, so store what you need.

Is this safe for bulk marketing? Treat it as a tool for conversations people expect. Unsolicited or repetitive bulk messages put an account at risk, so send only to consented contacts, validate numbers, and keep your volume modest.

Try it

Explore the full operation set on the WhatsApp Messaging page, and read the create-session, send-message and list-inbox references. Check pricing for per-request costs.