Skip to main content

bloonio_wa_relay_client

Client SDK for bloonio_wa_relay — the Bloonio WhatsApp transport PaaS. Send template/text/media messages, query numbers, templates, suppressions and the 24h customer-service window, and receive HMAC#1-signed webhook events, all via the same tenant_id + tenant_secret model as bloonio_auth_relay_client / bloonio_chat_relay_client / bloonio_mail_relay_client. Framework-agnostic core + thin FastAPI / Django adapters.

Install

pip install "bloonio-wa-relay-client[fastapi]"   # for FastAPI tenants
pip install "bloonio-wa-relay-client[django]"    # for Django tenants
pip install bloonio-wa-relay-client              # framework-agnostic core only

Two-minute integration

# .env
BLOONIO_WA_BASE_URL=https://wa-relay.example.com
BLOONIO_WA_TENANT_ID=<uuid>
BLOONIO_WA_TENANT_SECRET=sk_...
# optional — authenticate with a bwa_ API key (Bearer) instead of HMAC#1 signing
BLOONIO_WA_API_KEY=bwa_...
# main.py — construct the client once at startup and register it as the singleton
from fastapi import FastAPI
from bloonio_wa_relay_client import WaRelayClient, WaRelaySettings, set_wa_client

app = FastAPI()
set_wa_client(WaRelayClient(WaRelaySettings()))   # reads the BLOONIO_WA_* env vars above
# anywhere else — a route, a Celery task, a management command
from bloonio_wa_relay_client import get_wa_client

wa = get_wa_client()
wa.send(from_number_id="pn_abc123", to="243810000001", text="Hi!")

There is no from_env(app) adapter class here — set_wa_client() / get_wa_client() is the whole pattern, and it's identical under Django (call set_wa_client(...) from AppConfig.ready() or the top of a startup/settings module). Calling get_wa_client() before set_wa_client() raises RuntimeError with that exact fix in the message.

Sending messages

from bloonio_wa_relay_client import WaRelayClient, WaRelayError, WaRelaySettings

client = WaRelayClient(WaRelaySettings(
    base_url="https://wa-relay.example.com",
    tenant_id="...",          # from provisioning
    tenant_secret="sk_...",   # shown once at provisioning
))

# Free-form text is only accepted INSIDE the contact's 24h service window (they must have
# messaged this number within the last 24h) — otherwise 409 `outside_service_window`. Check
# fetch_window() first (below), or send a template (also below) to start the conversation cold.
res = client.send(from_number_id="pn_abc123", to="243810000001", text="Your table is ready!")
print(res["message_id"], res["status"])   # every method returns the relay's raw dict/list —
                                           # NOT a validated core.types model, see "Public surface"

# Templates work even outside the window, as long as they're `approved` (see Templates below).
# `template_components` is a flat list of positional {{1}}, {{2}}, ... values — sent on the wire
# as `template_variables`. There is no `template_components`/`components` field on send() itself;
# `components` means something different (Meta component blocks) on create_template(), below.
res = client.send(
    from_number_id="pn_abc123",
    to="243810000001",
    type="template",
    template_name="flight_cancelled",
    template_language="fr",
    template_components=["Alice", "AF123"],
    idempotency_key="order-9981-cancelled",   # optional; a replayed key returns the same result
)

# image / document sends: Meta requires media_url, and the relay does not validate it — an
# image send with no media_url is accepted here and fails only when Meta itself rejects it.
client.send(from_number_id="pn_abc123", to="243810000001", type="image",
            media_url="https://cdn.example.com/receipt.jpg")

Errors. Every method raises WaRelayError on any non-2xx response: .status_code, .message (the relay's message, or the raw response text when there's no structured message to show), .body (the parsed JSON envelope, or None when the response body wasn't JSON at all — raw text is never put here, only ever in .message), and .code when the relay's envelope carried one (number_not_owned, outside_service_window, recipient_suppressed, ... — FastAPI's own 422 validation-error shape has no code at all).

try:
    client.send(from_number_id="pn_abc123", to="243810000001", text="Hi")
except WaRelayError as e:
    print(e.status_code, e.code, e.message)

Reuse one WaRelayClient per process — it wraps a single httpx.Client — and either call client.close() when done or use it as a context manager: with WaRelayClient(...) as client:. The async twin, AsyncWaRelayClient, has full method parity (await client.send(...), async with AsyncWaRelayClient(...) as client: / await client.aclose()).

Messages, numbers & the service window

# your sent-message log — newest first; no cursor pagination, `limit` only (1-200, default 50)
for m in client.list_messages(limit=20):
    print(m["message_id"], m["status"])

# `message_id` here is the RELAY's own id (a uuidv7 — from send()'s or list_messages()'s
# result), deliberately NOT the `wamid` WhatsApp assigns. Webhook payloads carry `wamid`
# (WebhookEvent.wamid, see Webhook callbacks below) — passing THAT here 404s ("Unknown message").
msg = client.fetch_message(message_id=res["message_id"])

for n in client.list_numbers():
    print(n["phone_number_id"], n["display_number"], n["status"])
number = client.fetch_number(number_id="pn_abc123")

# would a free-form (non-template) send be accepted right now?
window = client.fetch_window(phone_number_id="pn_abc123", wa_id="243810000001")
if window["is_open"]:
    client.send(from_number_id="pn_abc123", to="243810000001", text="Still there?")
else:
    print(window["reason"])   # "never_messaged" | "window_expired"

Templates

# `phone_number_id` is required — every call 422s without it (CreateTemplateRequest has no
# default for it server-side, even though it's easy to forget when sketching this call).
tpl = client.create_template(
    phone_number_id="pn_abc123",
    name="flight_cancelled",
    language="fr",
    category="UTILITY",
    components=[{"type": "BODY", "text": "Hello {{1}}, your flight {{2}} was cancelled."}],
)
print(tpl["status"])   # "submitted" — only Meta moves it on to approved/rejected/paused

for t in client.list_templates():
    print(t["name"], t["language"], t["status"])

Suppressions

# the opt-out list for a number. STOP-keyword replies are suppressed automatically; this is
# for adding one by hand — the relay always records it with reason "manual" (no reason param).
client.suppress_contact(phone_number_id="pn_abc123", wa_id="243810000003")

for s in client.list_suppressions():
    print(s["wa_id"], s["reason"])   # "stop_keyword" | "manual"

Webhook callbacks

The relay delivers five events — the wa.message.* family — to your backend as HMAC#1-signed POSTs, retrying on any non-2xx response (quadratic backoff, up to 5 attempts). Mount them under whatever prefix you registered with the relay as your tenant's callback_url_base; the examples below use the contract's own /api/v1/wa-callbacks/ convention.

Path Event Fired from
POST /api/v1/wa-callbacks/wa-message-received wa.message.received an inbound WhatsApp message
POST /api/v1/wa-callbacks/wa-message-sent wa.message.sent Meta status callback sent
POST /api/v1/wa-callbacks/wa-message-delivered wa.message.delivered Meta status callback delivered
POST /api/v1/wa-callbacks/wa-message-read wa.message.read Meta status callback read
POST /api/v1/wa-callbacks/wa-message-failed wa.message.failed Meta status callback failed

FastAPI

from fastapi import FastAPI
from bloonio_wa_relay_client import WaRelaySettings, WebhookEventType
from bloonio_wa_relay_client.adapters.fastapi import build_callback_router

async def on_message_received(event):   # WebhookHandler — async only under FastAPI
    print(event.wamid, event.from_, event.text)

app = FastAPI()
app.include_router(
    build_callback_router(
        settings=WaRelaySettings(),
        handlers={WebhookEventType.MESSAGE_RECEIVED: on_message_received},
    ),
    prefix="/api/v1/wa-callbacks",
)

Django

# urls.py
from django.urls import include, path
from bloonio_wa_relay_client import WebhookEventType
from bloonio_wa_relay_client.adapters.django import build_callback_urlpatterns

def on_message_received(event):   # sync or async — both work
    print(event.wamid, event.from_, event.text)

urlpatterns = [
    path(
        "api/v1/wa-callbacks/",
        include(build_callback_urlpatterns(handlers={WebhookEventType.MESSAGE_RECEIVED: on_message_received})),
    ),
]

Django's build_callback_urlpatterns defaults settings to None, which reads BLOONIO_WA_* env vars for you (as shown above). FastAPI's build_callback_router has no such default — settings is required; passing None there survives construction and only fails with AttributeError on the first real webhook POST, so always build a WaRelaySettings() explicitly (as the FastAPI example above does). Past that difference, both adapters share the same semantics: 401 on missing/bad HMAC#1 headers (handlers never run), 400 on an invalid payload or a body whose event_type doesn't match the path, handler exceptions logged but the response is still 200 (so the relay doesn't retry a delivery your handler already received) — you own retries and idempotency from there. Events with no registered handler are still HMAC-verified, accepted, and return 200.

Signing. The same three headers used for outbound tenant-auth calls — X-Bloonio-Tenant-Id / X-Bloonio-Timestamp / X-Bloonio-Signature, identical HMAC#1 formula — there is no separate webhook-signing secret.

No idempotency, no replay window. Delivery is at-least-once, not exactly-once: the relay retries on any non-2xx response, so the same event can legitimately arrive more than once — always dedupe your handler on event.wamid (persist it yourself; this envelope has no event_id). There's also no replay cache on either side today — verification only checks that the signature matches the body and the declared timestamp, not that the timestamp is recent, so a captured valid payload would still verify tomorrow. Until a replay cache ships, don't rely on the signature alone to keep your callback URL private.

Public surface

Every client method returns the relay's response data verbatim — a plain dict or list, never an instance of the schemas below. The schemas exist so you can validate/type a response yourself (e.g. Message.model_validate(res)) or type a webhook payload — WebhookEvent is the one schema the adapters already parse for you.

Symbol Kind Notes
WaRelayClient client (sync) wraps httpx.Client; reuse one per process
AsyncWaRelayClient client (async) wraps httpx.AsyncClient; async with / await .aclose()
WaRelaySettings settings reads BLOONIO_WA_* env vars (pydantic-settings)
WaRelayError error raised on any non-2xx; .status_code / .message / .body / .code
WebhookEventType enum the five wa.message.* webhook events
MessageStatus enum queuedsentdeliveredread, or terminal failed
TemplateStatus enum draftsubmittedapproved | rejected | paused
SuppressionReason enum stop_keyword | manual
SendResult schema send()'s response shape
Message schema one row of list_messages() / fetch_message()
Number schema one row of list_numbers() / fetch_number()
Template schema one row of list_templates() / create_template()
Suppression schema one row of list_suppressions()
ContactWindow schema fetch_window()'s response shape
WebhookEvent schema the parsed payload your webhook handlers receive
set_wa_client / get_wa_client singleton app-startup wiring — see "Two-minute integration"

License

Proprietary — Bloonio internal.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bloonio_wa_relay_client-0.1.1.tar.gz (37.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bloonio_wa_relay_client-0.1.1-py3-none-any.whl (25.5 kB view details)

Uploaded Python 3

File details

Details for the file bloonio_wa_relay_client-0.1.1.tar.gz.

File metadata

  • Download URL: bloonio_wa_relay_client-0.1.1.tar.gz
  • Upload date:
  • Size: 37.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for bloonio_wa_relay_client-0.1.1.tar.gz
Algorithm Hash digest
SHA256 82329cea1f506486f59ee0c556ad4c1f3b0549dff2223d83048e78a7ec566bf7
MD5 09571da0844a2c8660c779cd1f6edfd8
BLAKE2b-256 40d54a4c062e197f953a26752fd48d924da0255ec0a7d4f1ea118e976f6a24b9

See more details on using hashes here.

File details

Details for the file bloonio_wa_relay_client-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for bloonio_wa_relay_client-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bff9f1b79db2ce5397bf343ef4cec5be58f692ac32bb687496c86c42648bdff4
MD5 51f7d5cf3c0ae3835eaad639c02c8b94
BLAKE2b-256 bfd04c5644fa24aa31451c6e1ae64ba7ef2480eabf193b135c4b5e947a8d1578

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page