Claude Code に API ドキュメントを渡したら、Zalo Webhook レシーバーを組み上げてくれた — UnifyPort
Zalo には公開 DM API がない。ベトナム市場をターゲットにしているなら──あるいはベトナムの顧客とやり取りするクロスボーダーチームなら──これが最初にぶつかる壁だ。公式ルートは Official Account の登録だが、あれは配信のための仕組み:審査認証、メッセージテンプレート、対話ウィンドウ、従量課金。顧客から届くメッセージを受け取りたいだけなら、OA は実際のニーズよりはるかに大きな問題を解決しようとしている。
そこで別の方法を試した。Claude Code を開き、UnifyPort の webhook リファレンスをコンテキストに貼り付けて、Python で Zalo webhook レシーバーを書くよう指示した。50分後にはFlaskサーバーが手元にあった:毎回のデリバリー署名を検証し、Zalo メッセージを構造化 JSON ファイルに記録し、Slack チャンネルに転送する。コード全体で約60行。
最終的に出来上がるもの
Python Flask サーバー。機能は以下の通り:
- UnifyPort webhook から
message.receivedイベントを受信 - HMAC-SHA256 と
signing_secretで毎回のデリバリー署名を検証 - 全 Zalo メッセージを構造化 JSON ファイルに記録
- Slack incoming webhook でメッセージ要約を Slack チャンネルに送信
所要時間:1時間弱。大部分は Claude Code の作業。必要なもの:Zalo アカウントを接続した UnifyPort ワークスペース(QR コードスキャンのみ──パスワード不要、法人認証不要)と Slack incoming webhook URL。
準備:ドキュメントを Claude Code のコンテキストに入れる
プロンプトの前に、API リファレンスをエージェントのコンテキストに入れる。Claude Code はローカルファイルを直接読めるので、最速の方法は:
- webhook イベントリファレンス──特に
message.receivedイベント構造、署名検証セクション、POST /v1/webhook-endpoints作成エンドポイント──をプロジェクトルートのunifyport-reference.mdにコピーする。 - または会話開始時に関連セクションを直接チャットに貼り付ける。
Claude Code が見るべき重要情報:message.received イベントの payload 構造、x-unifyport-signature ヘッダー、signing_secret を使った HMAC-SHA256 検証の仕組み。これがないとエージェントはフィールド名を推測する──そして必ず間違える。
他のツールでも同じ。Cursor では @Docs でリファレンスを指定。Windsurf ではドキュメントパネル。仕組みは同じ:正しいフィールド名をインプットすれば、正しいコードがアウトプットされる。
プロンプトごとに組み上げる
最初のプロンプト──スケルトン:
unifyport-reference.md を読んで。Flask で app.py を書いて、POST /webhook ルートを作って。event フィールドが “message.received” のとき、provider、from、text を標準出力に表示して。全イベントで 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)
ハンドラー7行。イベント構造が統一されているので、分岐は1フィールドだけ──プラットフォームごとのパースは不要。
2つ目のプロンプト──署名検証:
各 webhook デリバリーには x-unifyport-signature ヘッダーが付く。環境変数の signing_secret で HMAC-SHA256 検証して。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 は絶対に一致しない。プロンプトに「生のリクエストボディ」と書いたら、初回で正解を出した。
3つ目のプロンプト──構造化ログと Slack 転送:
2つ追加して:(1) 各 message.received イベントを messages.jsonl に追記、1行1 JSON オブジェクト、timestamp、provider、from、text を含める。(2) 環境変数 SLACK_WEBHOOK_URL の Slack incoming webhook に1行のサマリーを POST して。
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)
60行。署名検証、構造化ログ、Slack 転送が揃った。ハンドラー全体がプラットフォーム非依存──evt["provider"] で Zalo だと分かるが、コード自体はプラットフォームを判定しない。
サーバーを起動して、Zalo メッセージが届くのを見る
UnifyPort ワークスペースで Zalo アカウントを接続する──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 がサーバーを書いてくれたことではない。API の表面が十分にシンプルでエージェントのコンテキストに収まるとき、AI コーディングツールは一発で仕上げるということだ。1つのイベント構造、1つの署名方式、1つの送信エンドポイント──そういうターゲットなら、プロンプトを受けたエージェントは迷わない。UnifyPort API リファレンスをツールに読ませ、message.received のスキーマを渡せば、ランチ前に署名検証・ログ記録・メッセージ転送が揃った webhook レシーバーが手に入る──そしてコードを変えずに他の全チャンネルを追加できる。日本で特に需要が高い LINE も、同じハンドラーでそのまま受け取れる。