← Все статьи
Руководство

Я скормил 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-сервер, который:

  1. Принимает события message.received от webhook UnifyPort
  2. Проверяет подпись каждой доставки через HMAC-SHA256 с вашим signing_secret
  3. Записывает каждое Zalo-сообщение в структурированный JSON-файл
  4. Отправляет сводку в Slack-канал через incoming webhook

Затраты времени: менее часа, большую часть работы делает Claude Code. Вам понадобится: рабочее пространство UnifyPort с подключённым Zalo-аккаунтом (сканирование QR-кода — без паролей, без бизнес-верификации) и URL Slack incoming webhook.

Подготовка: загрузите документацию в контекст Claude Code

Перед началом работы передайте API-справочник агенту. Claude Code умеет читать локальные файлы, поэтому самый быстрый путь:

  1. Скопируйте справочник по webhook-событиям — в частности, структуру события message.received, раздел проверки подписи и эндпоинт POST /v1/webhook-endpoints — в файл unifyport-reference.md в корне проекта.
  2. Или вставьте нужные разделы прямо в чат при начале диалога.

Что 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-приёмник с проверкой подписи, логированием и пересылкой — а потом без изменений кода подключите все остальные каналы.