Skip to main content

rmyndharis-openwa

This document describes SDK 0.6.0. Send idempotency, webhook failure redrive, message-window filters, poll choices, last-message types, and the event.response webhook event name require OpenWA 0.25.0 or later.

Official Python SDK for OpenWA, the open-source WhatsApp API Gateway. OpenWA is an independent project, not affiliated with or endorsed by WhatsApp or Meta.

A synchronous client built on httpx, with bundled type hints (PEP 561).

Install

pip install rmyndharis-openwa

Requires Python 3.9+. The importable module is openwa.

Usage

from openwa import OpenWAClient

client = OpenWAClient(
    base_url="https://your-gateway.example.com",
    api_key="owa_k1_…",
)

# Sessions are addressed by the UUID that create() returns, not by name. Create a session once;
# afterwards, find its id with client.sessions.list({"name": "my-session"}).
session = client.sessions.create({"name": "my-session"})
client.sessions.start(session["id"])

# Link the account before sending: scan sessions.get_qr_code (or use sessions.request_pairing_code for a number with no linked session),
# then wait for status "ready". An unlinked session answers the send with 409.
result = client.messages.send_text(session["id"], {
    "chatId": "628123456789@c.us",
    "text": "Hello from the OpenWA Python SDK!",
})
print(result["messageId"])

The client is also a context manager (it closes the underlying connection pool on exit):

with OpenWAClient(base_url="…", api_key="…") as client:
    client.messages.send_text(session_id, {"chatId": "…@c.us", "text": "hi"})

For tests, pass an httpx transport — no global monkey-patching required:

import httpx
client = OpenWAClient(base_url="…", api_key="…", transport=httpx.MockTransport(handler))

GET /search is wrapped as client.search.search(params). Only q is required; the rest (sessionId, chatId, direction, type, from, dateFrom, dateTo, limit, offset) are optional. dateFrom / dateTo are epoch-ms. The active search provider (built-in DB full-text, or a plugin) answers; if none is configured the server returns 501.

res = client.search.search({"q": "invoice", "sessionId": session_id, "limit": 20})
for hit in res["hits"]:
    print(hit["snippet"], hit.get("score"))

score is provider-dependent and optional: the built-in provider on SQLite leaves it out for a key restricted to sessions or chats, so read it with get.

Messaging

Voice notes: pass ptt=True inside the body dict to send_audio to send a real WhatsApp voice note (PTT). Supply audio/ogg; codecs=opus audio for reliable playback; the server defaults the mimetype to that when ptt is set without one.

Errors

A non-2xx response raises a typed OpenWAApiError subclass — OpenWAAuthError (401), OpenWAForbiddenError (403), OpenWANotFoundError (404), OpenWAConflictError (409), OpenWARateLimitError (429), OpenWANotImplementedError (501), OpenWAServiceUnavailableError (503) — each carrying .status and the parsed .body. A timeout raises OpenWATimeoutError. 503 is transient, but a catalog 503 can persist because WhatsApp may never answer that query, so bound any retry. A 429 from the global rate limiter lifts when its window expires (seconds for the per-second tier, up to an hour for the hourly tier by default), and .retry_after_seconds carries its Retry-After header. A 429 whose .code is "SEND_PACING_LIMITED" is usually not transient: do not retry it before .retry_after_seconds, which then comes from the body: a few seconds when only sends still in flight caused it, the rest of the failure breaker's cooldown (SEND_PACING_BREAKER_COOLDOWN_MS, 15 minutes by default) after a run of send failures, otherwise up to the next UTC day. Every API error also exposes the response .headers. A 503 does not prove a write was never carried out: the engine answers it when WhatsApp did not confirm in time, and the change may still have been applied, so re-read the state before repeating it. In a routed deployment a forward that fails before reaching the owner node answers 503, one that fails after the request reached it answers 502 or 504, and a 503 from the owner itself is relayed unchanged.

from openwa import OpenWANotFoundError

try:
    client.sessions.get("00000000-0000-0000-0000-000000000000")
except OpenWANotFoundError as e:
    print(e.status)  # 404

Notes

  • Use HTTPS in production — the API key is sent as X-API-Key and is bearer-equivalent.
  • The SDK does not retry, and never follows redirects (so the key is never re-sent to a redirect target). Path segments are percent-encoded; a base-URL path prefix (e.g. behind a reverse proxy) is preserved.
  • Escape hatch for endpoints the SDK does not wrap: client.request(method, path, query=…, body=…).

Receiving webhooks

A webhook configured with a secret signs each delivery in its X-OpenWA-Signature header. Check it with verify_webhook_signature against the raw, unparsed request body (bytes or str), exactly as received, and parse the JSON only after the check passes: a re-serialized body can differ byte for byte and will not verify. The helper returns False (never raises) for a missing, malformed or non-matching signature. WebhookDelivery (in openwa.types) types the parsed body.

import json

from flask import Flask, request
from openwa import verify_webhook_signature
from openwa.types import WebhookDelivery

app = Flask(__name__)


@app.post("/openwa/webhook")
def openwa_webhook():
    raw_body = request.get_data()
    if not verify_webhook_signature(raw_body, request.headers.get("X-OpenWA-Signature"), secret):
        return "Invalid signature", 401
    delivery: WebhookDelivery = json.loads(raw_body)
    # Process delivery["event"] and delivery["data"] here.
    return "OK", 200

Releasing

Publishing to PyPI is done by the python-sdk-release.yml workflow, which authenticates with PyPI Trusted Publishing (OIDC). There is no PyPI token in the workflow or in the repository secrets: PyPI mints a short-lived credential from the GitHub OIDC token, so nothing long-lived exists to leak or rotate.

One-time setup, required before the first tag — on pypi.org, open the project's publishing settings and add a GitHub trusted publisher:

  • Owner: rmyndharis
  • Repository: OpenWA
  • Workflow name: python-sdk-release.yml

There are no repository secrets to add. Until the trusted publisher exists PyPI rejects the upload, so configure it first.

Cutting a release:

  1. Bump version in pyproject.toml and land it on main.
  2. Tag that commit py-sdk-v<version> (e.g. py-sdk-v0.6.0) and push the tag. The SDK has its own version line — the monorepo's v* tags are the app version and never trigger an SDK publish.
  3. The workflow re-runs the test suite, builds the sdist and wheel, and uploads. The artifacts published are the ones those tests passed against.

License

MIT

Send idempotency

The example below requires SDK 0.6.0 or later and OpenWA 0.25.0 or later. SDK 0.5.1 does not expose this per-send key argument.

Supply a unique caller-owned key for one logical send and reuse it when retrying that same call. Keys contain 1-255 visible ASCII characters; unrelated calls must use different keys. The SDK forwards the key without changing its retry policy or generating one automatically.

client.messages.send_text(session_id, {"chatId": "628123@c.us", "text": "Hello"}, idempotency_key="order-123")

The gateway supports text, image, video, audio, document, sticker, location, contact, template, poll, reply and forward sends. Completed calls replay their response; a 409 with the error code IDEMPOTENCY_KEY_IN_PROGRESS means the first request is still running, so retry with the same key later; a 409 IDEMPOTENCY_OUTCOME_UNKNOWN means the message may have been sent, so check the chat and, if it did not arrive, send again with a new key (the code tells them apart from the 409 for a session that is not connected). A changed request with the same key returns 422. The gateway retains keys for 24 hours. Bulk, product and button operations have no such guarantee. Omit the key to preserve existing behavior.

Metadata

Release files for rmyndharis-openwa 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rmyndharis-openwa 0.6.0
File Size Uploaded
rmyndharis_openwa-0.6.0.tar.gz 59.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rmyndharis-openwa 0.6.0
File Interpreter ABI Platform
rmyndharis_openwa-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 111.1 kB

Release files / rmyndharis_openwa-0.6.0.tar.gz

Download URL rmyndharis_openwa-0.6.0.tar.gz
Size 59.7 kB
Tags Source
SHA-256 checksum
How to use checksums
958c1cd2413d6ebdd17de0b8a1c9e211b2c27c75277a0bf471ba50cc6e86bd84
BLAKE2b-256 checksum
How to use checksums
174f11fe51a37906116b3ecb7763cbe47067b1530b7141b93f48e6fd3d4b1aa7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / rmyndharis_openwa-0.6.0-py3-none-any.whl

Download URL rmyndharis_openwa-0.6.0-py3-none-any.whl
Size 51.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0ccf5d382cff7eb9d682fef719e0ba811172e55a3290806463cd766ad8dc6a3e
BLAKE2b-256 checksum
How to use checksums
fa55755a51b396a9eda5a4e3c6a428bc9f9cadf3ec4032987563cefc8cd726c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page