Wazzapo для Python
Официальная библиотека для API v2 Wazzapo: сообщения WhatsApp, шаблоны, контакты, сделки, переписки и вебхуки. Лицензия MIT.
- подсказки в редакторе по-русски: у каждого метода и поля описание из документации API;
- повторы на 429, 5xx и сбоях сети с учётом
Retry-After; Idempotency-Keyна каждый POST: повтор не отправит сообщение дважды;- ошибки отдельными классами с
code,message,details,request_id; - списки: одна страница или все подряд итератором;
- проверка подписи вебхуков;
- синхронный и async клиент, одна зависимость (httpx), Python 3.9+.
Установка
pip install wazzapo
Первое сообщение за 5 минут
- В кабинете Wazzapo откройте раздел «API» и создайте ключ. Для пробы берите тестовый ключ
wz_test_…: он ничего не отправляет и не меняет, а отвечает как настоящий. - Положите ключ в переменную окружения:
export WAZZAPO_API_KEY=wz_test_...
- Узнайте
phone_number_idсвоего номера и отправьте сообщение:
from wazzapo import Wazzapo
client = Wazzapo() # ключ из WAZZAPO_API_KEY, можно передать Wazzapo(api_key="...")
# Только номера WhatsApp (без фильтра в списке все каналы: Telegram, OLX, почта...)
numbers = client.phone_numbers.list(channel="whatsapp")["data"]
whatsapp = next(n for n in numbers if n["status"] == "connected")
phone_number_id = whatsapp["phone_number_id"]
# Текст доходит, только если клиент писал вам за последние 24 часа
sent = client.messages.send({
"phone_number_id": phone_number_id,
"to": "77071234567",
"type": "text",
"text": {"body": "Айгуль, здравствуйте! Запись на 25 октября в 15:00 подтверждена."},
})
print(sent["message_id"])
- Первое сообщение клиенту (или через 24 часа тишины) отправляйте одобренным шаблоном:
client.messages.send({
"phone_number_id": phone_number_id,
"to": "77071234567",
"type": "template",
"template": {
"name": "zapis_napominanie",
"language": {"code": "ru"},
"components": [
{
"type": "body",
"parameters": [
{"type": "text", "text": "Айгуль"},
{"type": "text", "text": "25 октября, 15:00"},
],
}
],
},
})
Список шаблонов и их статус: client.templates.list(phone_number_id=phone_number_id, status="APPROVED").
Как устроены методы
client.<ресурс>.<действие>(...):client.contacts.create,client.deals.move,client.phone_numbers.get_business_profile.- id из адреса передаётся первым аргументом:
client.contacts.get("6a7d..."). - Тело запроса: словарь первым аргументом после id (
client.contacts.update("6a7d...", {"name": "Айгуль"})) илиbody=; поля подсказывает редактор. Параметры списка: именованные аргументы. - Ответ: обычный словарь, как в документации. Типы для своего кода:
from wazzapo.types import SendMessageIn, DealOut. - У каждого метода есть настройки одного вызова:
timeout=(секунды),headers=(свои заголовки, например{"X-Request-Id": "order-4521"}; ключ иIdempotency-Keyони не заменяют),max_retries=; у POST ещёidempotency_key=.
deal = client.deals.create(
{
"pipeline_id": "6712a0c4e1b2c3d4e5f60718",
"title": "Заявка с сайта: кухня на заказ",
"contact": {"name": "Айгуль Сейткали", "phone": "77071234567"},
"amount": 15000000, # в тиынах: 150 000 тенге
"currency": "KZT",
"external_id": "site-order-4521",
},
idempotency_key="site-order-4521", # повтор с тем же ключом не создаст вторую сделку
)
Загрузка и скачивание файлов:
# путь, bytes или открытый файл; путь к несуществующему файлу сразу даёт FileNotFoundError
media = client.media.upload("schet_1024.pdf", phone_number_id=phone_number_id)
client.messages.send({"phone_number_id": phone_number_id, "to": "77071234567", "type": "document",
"document": {"id": media["media_id"], "filename": "schet_1024.pdf"}})
data: bytes = client.media.download(media["media_id"], phone_number_id=phone_number_id)
Ошибки
from wazzapo import OutsideWindowError, RateLimitError, ValidationError, WazzapoError
try:
client.messages.send({...})
except OutsideWindowError:
... # клиент не писал 24 часа: отправьте шаблон
except ValidationError as e:
print(e.fields) # [{"field": "body.to", "message": "Field required"}]
except RateLimitError as e:
print(e.retry_after) # SDK уже повторил запрос max_retries раз
except WazzapoError as e:
print(e.status, e.code, e.message, e.request_id) # request_id сообщите в поддержку
| Класс | Когда |
|---|---|
BadRequestError |
400: неверный запрос |
AuthenticationError |
401: нет ключа или он неверный |
PaymentRequiredError |
402: тариф не оплачен или Meta не может списать оплату |
PermissionDeniedError |
403: нет права у ключа, IP не разрешён |
UsageLimitError |
403 USAGE_LIMIT_EXCEEDED: исчерпан лимит тарифа |
NotFoundError |
404 |
ConflictError |
409: объект уже есть, например contact_exists (id в details) |
ValidationError |
422 VALIDATION_ERROR: поля не прошли проверку, список в fields |
OutsideWindowError |
422 outside_24h_window |
TemplateError |
422 template_*: шаблон не найден, не одобрен, приостановлен |
UnprocessableEntityError |
остальные 422, например invalid_parameters |
RateLimitError |
429 |
ServerError |
5xx |
APIConnectionError, APITimeoutError |
ответа не было: code равен CONNECTION_ERROR или TIMEOUT |
PaginationError |
сервер вернул тот же next_cursor ещё раз, обход списка остановлен |
WebhookVerificationError |
вебхук не прошёл проверку (см. «Вебхуки») |
Все классы наследуют WazzapoError. Если ответ пришёл не от API (например, страница прокси с 502), code берётся по статусу: BAD_GATEWAY, SERVICE_UNAVAILABLE и т.д.
Все коды и их значения: раздел «Коды ошибок» в документации.
Списки
page = client.contacts.list(tag="vip", limit=100) # одна страница: data, next_cursor, has_more
if page["has_more"]:
next_page = client.contacts.list(tag="vip", limit=100, cursor=page["next_cursor"])
for contact in client.contacts.list_all(tag="vip"): # все страницы, по одной за раз
print(contact["phone"])
Async
import asyncio
from wazzapo import AsyncWazzapo
async def main() -> None:
async with AsyncWazzapo() as client:
await client.messages.send({...})
async for deal in client.deals.list_all(pipeline_id="6712a0c4e1b2c3d4e5f60718"):
print(deal["title"])
asyncio.run(main())
Вебхуки
Создайте вебхук и сохраните секрет: он показывается один раз.
hook = client.webhooks.create({"url": "https://example.kz/wazzapo", "event_types": ["message.incoming", "deal.created"]})
secret = hook["secret"]
Проверяйте подпись на сыром теле запроса, до разбора JSON. verify сверяет подпись и время (допуск 5 минут) и возвращает событие. Без секрета (пустая строка или None, например не задана переменная окружения) verify сразу бросает WebhookVerificationError: так поддельный запрос не пройдёт. Та же ошибка, если подписи нет, она чужая, запрос старый или тело не JSON.
FastAPI:
from fastapi import FastAPI, HTTPException, Request
from wazzapo import webhooks
app = FastAPI()
@app.post("/wazzapo")
async def wazzapo_hook(request: Request) -> dict[str, bool]:
try:
event = webhooks.verify(await request.body(), request.headers, secret)
except webhooks.WebhookVerificationError:
raise HTTPException(status_code=400)
if event["event"] == "message.incoming":
print(event["data"]["message"]["text"])
return {"ok": True}
Flask:
from flask import Flask, abort, request
from wazzapo import webhooks
app = Flask(__name__)
@app.post("/wazzapo")
def wazzapo_hook() -> dict[str, bool]:
try:
# verify принимает любые заголовки с items(): request.headers Flask, dict, заголовки FastAPI
event = webhooks.verify(request.get_data(), request.headers, secret)
except webhooks.WebhookVerificationError:
abort(400)
return {"ok": True}
Ответьте 2xx быстрее 10 секунд, долгую работу делайте в фоне. Одно событие может прийти дважды (повтор доставки): у всех повторов один и тот же заголовок X-Wazzapo-Delivery-Id (webhooks.DELIVERY_ID_HEADER), по нему и отсеивайте дубли.
delivery_id = request.headers.get(webhooks.DELIVERY_ID_HEADER)
if already_processed(delivery_id): # ваша проверка: таблица, Redis и т.п.
return {"ok": True}
Для своих тестов подпись ставит webhooks.sign(body, secret), а webhooks.verify(..., now=...) проверяет на заданное время.
Примеры
Готовые примеры без фреймворков в папке examples:
- bot_buttons.py: бот с кнопками отвечает на входящее сообщение и на нажатие кнопки;
- lead_to_deal.py: заявка с формы сайта становится сделкой без дублей, клиент получает приветственный шаблон;
- webhooks_receiver.py: приёмник всех событий с проверкой подписи.
Там же описано, как проверить их на тестовом ключе и подписанным тестовым событием.
Тестовый ключ
Ключ wz_test_… проходит все проверки, но ничего не отправляет в WhatsApp и не меняет данные: сообщения получают id wamid.SANDBOX…. Чтение работает по-настоящему. client.is_test показывает, какой ключ в работе.
Настройки
Wazzapo(
api_key="wz_live_...", # или WAZZAPO_API_KEY
base_url="https://wazzapo.kz", # или WAZZAPO_BASE_URL
timeout=30, # секунды на запрос
max_retries=2, # повторы на 429, 5xx и сбоях сети
http_client=None, # свой httpx.Client: прокси, свои заголовки
)
Ключ не попадает в логи и repr (ни клиента, ни его настроек): видны только первые 8 знаков, они же в client.api_key_hint. Отладка запросов: logging.getLogger("wazzapo").setLevel(logging.DEBUG).
Разработка
Код в src/wazzapo/_generated собирается из описания API: cd sdk && python -m codegen python. Руками его не правят.
uv venv && uv pip install -e ".[dev]"
pytest
mypy src/wazzapo
WAZZAPO_TEST_API_KEY=wz_test_... pytest tests/test_live.py
Metadata
Release files for wazzapo 0.1.1
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 | |
|---|---|---|---|---|
| wazzapo-0.1.1.tar.gz | 114.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wazzapo-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 194.0 kB
Release files / wazzapo-0.1.1.tar.gz
| Download URL | wazzapo-0.1.1.tar.gz |
|---|---|
| Size | 114.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f3fbcd9cb4d1aaf1b431e34ab070b998a389560fa98e1d7eb6805778f0c484c
|
|
BLAKE2b-256 checksum How to use checksums |
57af865dfb5c474cf331f5d7b1baafaf48b68fb313bf70009f4fc335d95a0b34
|
| 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 19, 2026.
Transparency logRelease files / wazzapo-0.1.1-py3-none-any.whl
| Download URL | wazzapo-0.1.1-py3-none-any.whl |
|---|---|
| Size | 79.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b13548fc438d81ae6394590dff3e111bec59092b8305a2278df5ba4be3828e2d
|
|
BLAKE2b-256 checksum How to use checksums |
c05f07311e4acbe0e9832db36baa3a4c4571a26afda87dac8851fd5d880295b9
|
| 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 19, 2026.
Transparency logSecurity for wazzapo 0.1.1
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 wazzapo-0.1.1.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 19, 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.