Integration
Messaging from Python, blocking or not
One package with two clients: a blocking one for scripts and cron jobs, an
asyncio one for bots and
web handlers. Same method names, same arguments, same errors — a test enforces that
the two cannot drift apart.
Install
pip install waapi
One dependency (httpx), tested on
Python 3.9 through 3.13.
The same call, either way
Start with the blocking client. When the code moves into an async web handler, the
call keeps its shape — the class name and an await
are the whole difference.
Blocking
from waapi import WaAPI
client = WaAPI(token=TOKEN, instance_id=123)
client.send_message(
chat_id="4915112345678@c.us",
message="Deployment finished.",
)
asyncio
from waapi import AsyncWaAPI
async with AsyncWaAPI(token=TOKEN, instance_id=123) as client:
await client.send_message(
chat_id="4915112345678@c.us",
message="Deployment finished.",
)
The chat ID suffix decides where the message lands:
@c.us for one person,
@g.us for a group,
@newsletter for a channel.
A wrong suffix is accepted and delivers nothing, so it is worth getting right first.
All 122 actions, not a selection
The methods are generated from the same OpenAPI specification the n8n node and the MCP server are built from, so the SDK tracks the API instead of falling behind it. Every action the REST API exposes is a typed method with named arguments — not a hand-picked subset that ages.
client.create_group(group_name="Ops", group_participants=["4915112345678@c.us"])
client.send_media(chat_id="4915112345678@c.us", media_url="https://example.com/report.pdf")
client.get_chats(limit=50)
client.get_contacts()
An action we shipped after the last SDK release is still reachable, by name, without waiting for anything:
client.action("some-new-action", {"chatId": "4915112345678@c.us"})
Typed, and honest about failure
Your type checker sees it
The package ships a py.typed
marker, so mypy and Pyright read the annotations instead of treating every
call as Any.
Optional parameters are keyword-only, which means a misplaced positional
argument is an error at check time rather than a field that quietly arrives
in the wrong place.
A 200 is not proof it was sent
When a connected account has dropped off, the API answers
200 with an error
body. Code that checks only the status code treats that as delivered. The SDK
raises FailedActionError
on it, the same class as a rejected request — because there is nothing
different a caller can do about the two.
from waapi import AuthenticationError, RateLimitError, FailedActionError
try:
client.send_message(chat_id="4915112345678@c.us", message="Hi")
except AuthenticationError:
... # token wrong, expired, or missing scopes
except RateLimitError as error:
time.sleep(error.retry_after or 5)
except FailedActionError:
... # accepted, but not carried out
ValidationError,
NotFoundError and
ServerError cover the rest.
All inherit from WaAPIError,
so one except catches
everything the SDK raises.
Python questions
What the SDK does and where its limits are.
-
3.9 and up. Every release is tested against 3.9, 3.10, 3.11, 3.12 and 3.13 before it is published.
-
No, both ship in the same package and share a method surface that a test enforces. Import WaAPI for blocking code and AsyncWaAPI inside an event loop; the arguments and the exceptions are identical.
-
One: httpx, which provides both the blocking and the asyncio transport. That is the reason for the choice — a separate library for each would have meant two request paths that could disagree about what counts as a failure.
-
The methods are generated from our OpenAPI specification, so a new endpoint becomes a typed method in the next release. Until then it is callable by name through client.action(name, payload), so nothing blocks on an SDK release.
-
Through webhooks rather than the SDK. Point your instance webhook at an HTTP endpoint in your own application; the payload carries the chat ID, which you hand straight back to send_message to reply.
-
No. Billing is a flat rate per connected account, regardless of how many messages or API calls you make.
-
Yes, MIT licensed, on GitHub. Issues and pull requests are welcome, though the generated method surface is regenerated from the spec rather than edited by hand.
Connect an account and start sending
One flat rate per connected account. Unlimited messages, no approval process, no per-message invoice.