Я скормил API-документацию Claude Code — и он собрал Zalo Webhook-приёмник — UnifyPort
У Zalo нет публичного API для личных сообщений. Если вы работаете на вьетнамский рынок — или в любой международной команде, которая общается с вьетнамскими клиентами, — это первая стена, в которую вы упираетесь. Есть официальный путь через Official Account, но это система рассылок: верификация, шаблоны сообщений, окна взаимодействия, поштучная тарификация. Если вам нужно просто получать сообщения, которые клиенты отправляют первыми, OA решает задачу намного масштабнее вашей реальной потребности.
Поэтому я сделал иначе. Открыл Claude Code, вставил в контекст справочник UnifyPort по webhook-событиям и попросил написать Zalo webhook-приёмник на Python. Через пятьдесят минут у меня был Flask-сервер: проверка подписи каждой доставки, запись Zalo-сообщений в структурированный JSON-файл и пересылка в Slack-канал. Всего около 60 строк кода.
Что получится в итоге
Python Flask-сервер, который:
- Принимает события
message.receivedот webhook UnifyPort - Проверяет подпись каждой доставки через HMAC-SHA256 с вашим
signing_secret - Записывает каждое Zalo-сообщение в структурированный JSON-файл
- Отправляет сводку в Slack-канал через incoming webhook
Затраты времени: менее часа, большую часть работы делает Claude Code. Вам понадобится: рабочее пространство UnifyPort с подключённым Zalo-аккаунтом (сканирование QR-кода — без паролей, без бизнес-верификации) и URL Slack incoming webhook.
Подготовка: загрузите документацию в контекст Claude Code
Перед началом работы передайте API-справочник агенту. Claude Code умеет читать локальные файлы, поэтому самый быстрый путь:
- Скопируйте справочник по webhook-событиям — в частности, структуру события
message.received, раздел проверки подписи и эндпоинтPOST /v1/webhook-endpoints— в файлunifyport-reference.mdв корне проекта. - Или вставьте нужные разделы прямо в чат при начале диалога.
Что Claude Code обязательно должен увидеть: структуру payload message.received, заголовок x-unifyport-signature и механизм проверки HMAC-SHA256 с signing_secret. Без этого агент будет угадывать имена полей — и угадает неправильно.
Другие инструменты работают так же. В Cursor используйте @Docs, в Windsurf — панель документации. Механика одинаковая: реальные имена полей на входе — корректный код на выходе.
Сборка по шагам, промпт за промптом
Первый промпт — скелет:
Прочитай unifyport-reference.md. Напиши Flask-сервер в app.py с маршрутом POST /webhook. Когда поле event равно “message.received”, выводи provider, from и text в stdout. Для всех событий возвращай 200.
Claude Code читает справочник и генерирует:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
evt = request.get_json()
if evt.get("event") == "message.received":
print(f"[{evt['provider']}] {evt['from']}: {evt['text']}")
return "", 200
if __name__ == "__main__":
app.run(port=3000)
Семь строк обработчика. Поскольку структура события унифицирована, нужна ветка по одному полю — никакого парсинга для отдельных платформ.
Второй промпт — проверка подписи:
Каждая доставка webhook содержит заголовок x-unifyport-signature. Проверяй его через HMAC-SHA256 с signing_secret из переменной окружения. HMAC должен вычисляться по сырому телу запроса, а не по ресериализованному JSON. При ошибке верификации возвращай 401. Используй сравнение за постоянное время.
Этот шаг превращает игрушку в продакшн-решение. Claude Code реализует правильно, потому что прочитал справочник:
import hmac
import hashlib
import os
SIGNING_SECRET = os.environ["UNIFYPORT_SIGNING_SECRET"]
def verify_signature(req):
signature = req.headers.get("x-unifyport-signature", "")
expected = hmac.new(
SIGNING_SECRET.encode(),
req.get_data(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
req.get_data() возвращает сырые байты до того, как Flask распарсит JSON — эта деталь критически важна. Если бы агент использовал request.get_json() и ресериализовал, HMAC никогда бы не совпал. В промпте было написано «сырое тело запроса» — и с первого раза получилось правильно.
Третий промпт — структурированное логирование и Slack-пересылка:
Добавь две вещи: (1) дописывай каждое событие message.received в файл messages.jsonl, по одному JSON-объекту на строку, с полями timestamp, provider, from, text. (2) Отправляй однострочную сводку на Slack incoming webhook из переменной окружения SLACK_WEBHOOK_URL.
Claude Code добавляет оба компонента за один проход. Готовый app.py:
import hmac
import hashlib
import json
import os
import requests
from flask import Flask, request
app = Flask(__name__)
SIGNING_SECRET = os.environ["UNIFYPORT_SIGNING_SECRET"]
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL", "")
def verify_signature(req):
signature = req.headers.get("x-unifyport-signature", "")
expected = hmac.new(
SIGNING_SECRET.encode(),
req.get_data(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route("/webhook", methods=["POST"])
def webhook():
if not verify_signature(request):
return "", 401
evt = request.get_json()
if evt.get("event") == "message.received":
record = {
"timestamp": evt["timestamp"],
"provider": evt["provider"],
"from": evt["from"],
"text": evt.get("text", ""),
"message_id": evt["message_id"],
}
with open("messages.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
if SLACK_WEBHOOK_URL:
requests.post(SLACK_WEBHOOK_URL, json={
"text": f"[{evt['provider']}] {evt['from']}: {evt.get('text', '')}"
})
return "", 200
if __name__ == "__main__":
app.run(port=3000)
Шестьдесят строк: проверка подписи, структурированное логирование, Slack-пересылка. Весь обработчик платформо-независим — evt["provider"] сообщает, что это Zalo, но код не разветвляется по платформам.
Запускаем и наблюдаем, как приходит Zalo-сообщение
Подключите Zalo-аккаунт в рабочем пространстве UnifyPort — Zalo использует QR-авторизацию: сканируете код в приложении Zalo, и через несколько секунд аккаунт подключён. Без бизнес-верификации, без регистрации OA, без управления паролями. Зарегистрируйте webhook-эндпоинт с subscribed_events: ["message.received"] и signing_secret.
Запустите сервер и отправьте сообщение с Zalo-контакта. Событие придёт в таком виде:
{
"event": "message.received",
"account_id": "acct_4Xn9kL",
"provider": "zalo",
"from": "user_7b2e4f",
"text": "Sản phẩm này còn hàng không ạ?",
"timestamp": 1750262400,
"message_id": "zalo_msg_9d3a1c"
}
Сервер проверяет подпись, добавляет запись в messages.jsonl и отправляет сводку в Slack. Если проверка подписи возвращает 401 — убедитесь, что UNIFYPORT_SIGNING_SECRET совпадает с настройкой webhook-эндпоинта.
Расширяем: добавляем WhatsApp без изменения обработчика
Вот где унифицированный webhook оправдывает себя. Подключите WhatsApp-аккаунт в том же рабочем пространстве, подпишите тот же webhook-эндпоинт — и тот же маршрут /webhook обработает обе платформы. WhatsApp-сообщение приходит в той же структуре message.received — отличается только поле provider:
{
"event": "message.received",
"account_id": "acct_2Rm7wQ",
"provider": "whatsapp",
"from": "user_5c8d3a",
"text": "Do you ship to Thailand?",
"timestamp": 1750262460,
"message_id": "wa_msg_6f1b2e"
}
Новый обработчик не нужен. Условный импорт не нужен. WhatsApp-специфичный парсинг не нужен. В messages.jsonl теперь лежат сообщения из Zalo и WhatsApp в одной схеме, а Slack-канал показывает оба потока. Добавление LINE или Telegram позже — та же история: подключаете аккаунт, и существующий обработчик всё обрабатывает.
Суть не в том, что Claude Code написал за вас сервер. Суть в том, что AI-инструмент для кодирования справляется чисто, когда API-поверхность достаточно проста, чтобы уместиться в контексте. Одна структура событий, одна схема подписи, один эндпоинт отправки — с такой целью промптированный агент не ошибётся. Скормите вашему инструменту справочник UnifyPort API, передайте схему message.received, и до обеда у вас будет webhook-приёмник с проверкой подписи, логированием и пересылкой — а потом без изменений кода подключите все остальные каналы.