All integrations

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.