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))
Search
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=Trueinside the body dict tosend_audioto send a real WhatsApp voice note (PTT). Supplyaudio/ogg; codecs=opusaudio for reliable playback; the server defaults the mimetype to that whenpttis 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-Keyand 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:
- Bump
versioninpyproject.tomland land it onmain. - 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'sv*tags are the app version and never trigger an SDK publish. - 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)
| File | Size | Uploaded | |
|---|---|---|---|
| rmyndharis_openwa-0.6.0.tar.gz | 59.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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