rmyndharis-openwa
This document describes SDK 0.7.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. The message.receipt and poll.vote webhook event names require OpenWA 0.26.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
means the gateway could not complete the request right now: the path to WhatsApp failed, the gateway
is draining or short of a dependency, or, on Baileys, WhatsApp throttled (code 429) or timed out
(code 408) the request. Back off and retry later instead of at once, because repeating a throttled
request straight away prolongs the throttle; 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 always prove a write was never carried out: when WhatsApp timed out, the
change may still have been applied, so re-read the state before repeating it. A throttle on a send
or a single write is the exception: it was turned away before it ran, so nothing was sent or
applied, and a keyed send frees its Idempotency-Key. A group settings update with several fields
applies them one at a time, so a 503 on a later field, a throttle included, leaves the earlier ones
applied; its message lists them after already applied:, so re-read the settings before retrying,
or send one field per request. 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.7.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.
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.
License
MIT
Metadata
Release files for rmyndharis-openwa 0.7.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 | Provenance | Actions |
|---|---|---|---|---|
| rmyndharis_openwa-0.7.0.tar.gz | 64.0 kB | With provenance | Details for rmyndharis_openwa-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| rmyndharis_openwa-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for rmyndharis_openwa-0.7.0-py3-none-any.whl |
Total release size: 117.1 kB
Release files / rmyndharis_openwa-0.7.0.tar.gz
| Download URL | rmyndharis_openwa-0.7.0.tar.gz |
|---|---|
| Size | 64.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ed7cb19810344dd0af271601c5bb9d58b097aa7bf33bc80faa53188915c375a2
|
|
BLAKE2b-256 checksum How to use checksums |
f32f054b5209896f5397751499d2773ee8718063dbd1890a17b4964f8447396e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 28, 2026.
Transparency logRelease files / rmyndharis_openwa-0.7.0-py3-none-any.whl
| Download URL | rmyndharis_openwa-0.7.0-py3-none-any.whl |
|---|---|
| Size | 53.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6fc0f6ef410abde63b6f9c4e7748fd652eb92a459f7bc0e96736d7f7e6622d84
|
|
BLAKE2b-256 checksum How to use checksums |
5834f22bcfa92cc44eaa8e321da23cdca445797e7551784be35ae87219fb31d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 28, 2026.
Transparency logSecurity for rmyndharis-openwa 0.7.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for rmyndharis_openwa-0.7.0.tar.gz
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 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.