← 全記事
チュートリアル

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 サーバー。機能は以下の通り:

  1. UnifyPort webhook から message.received イベントを受信
  2. HMAC-SHA256 と signing_secret で毎回のデリバリー署名を検証
  3. 全 Zalo メッセージを構造化 JSON ファイルに記録
  4. Slack incoming webhook でメッセージ要約を Slack チャンネルに送信

所要時間:1時間弱。大部分は Claude Code の作業。必要なもの:Zalo アカウントを接続した UnifyPort ワークスペース(QR コードスキャンのみ──パスワード不要、法人認証不要)と Slack incoming webhook URL。

準備:ドキュメントを Claude Code のコンテキストに入れる

プロンプトの前に、API リファレンスをエージェントのコンテキストに入れる。Claude Code はローカルファイルを直接読めるので、最速の方法は:

  1. webhook イベントリファレンス──特に message.received イベント構造、署名検証セクション、POST /v1/webhook-endpoints 作成エンドポイント──をプロジェクトルートの unifyport-reference.md にコピーする。
  2. または会話開始時に関連セクションを直接チャットに貼り付ける。

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 も、同じハンドラーでそのまま受け取れる。