← 所有文章
教學

我把 API 文件丟給 Claude Code,它幫我搭了一個 Zalo Webhook 接收器 — UnifyPort

Zalo 沒有公開的私訊 API。如果你的業務面向越南市場——或者任何需要和越南客戶溝通的跨境團隊——這是你碰到的第一堵牆。官方路徑是註冊 Official Account,但那是一套廣播體系:認證審核、訊息模板、互動視窗期、按則計費。如果你只需要接收客戶主動發來的訊息,OA 解決的是一個比你實際需求大得多的問題。

所以我換了個思路。打開 Claude Code,把 UnifyPort 的 webhook 參考文件貼進上下文,讓它用 Python 寫一個 Zalo webhook 接收器。五十分鐘後,我拿到了一個 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 頻道

時間成本:不到一小時,大部分是 Claude Code 在做事。你需要一個 UnifyPort 工作空間,裡面連接一個 Zalo 帳號(掃 QR code 即可——不需要提供密碼,不需要企業驗證),以及一個 Slack incoming webhook URL。

準備工作:把文件放進 Claude Code 的上下文

動手之前,先把 API 文件餵給 agent。Claude Code 可以直接讀取本機檔案,最快的方式是:

  1. 把 webhook 事件參考文件——特別是 message.received 事件結構、簽章驗證部分、以及 POST /v1/webhook-endpoints 建立介面——複製到專案根目錄的 unifyport-reference.md 檔案裡。
  2. 或者在開始對話時,直接把相關內容貼進聊天視窗。

Claude Code 必須看到的關鍵內容:message.received 事件的 payload 結構、x-unifyport-signature 請求標頭、以及 HMAC-SHA256 配合 signing_secret 的驗證方式。少了這些,agent 會猜欄位名——而且一定猜錯。

其他 AI 程式碼工具同理。在 Cursor 裡用 @Docs 指向參考文件,在 Windsurf 裡用文件面板。核心邏輯一樣:餵進去真實的欄位名,輸出正確的程式碼。

逐步建構,一個 prompt 一個 prompt 來

第一個 prompt——骨架:

讀 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)

七行處理邏輯。因為事件結構是統一的,只需要判斷一個欄位——不需要針對不同平台做解析。

第二個 prompt——簽章驗證:

每次 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 之前的原始位元組——這個細節至關重要。如果 agent 用了 request.get_json() 然後重新序列化,HMAC 永遠對不上。提示詞裡寫了「原始請求本體」,它第一次就寫對了。

第三個 prompt——結構化日誌和 Slack 轉發:

加兩個功能:(1) 把每個 message.received 事件追加寫入 messages.jsonl 檔案,每行一個 JSON 物件,包含 timestamp、provider、from、text。(2) 把一行摘要發到環境變數 SLACK_WEBHOOK_URL 指定的 Slack incoming webhook。

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 轉發全部就位。整個 handler 是平台無關的——evt["provider"] 告訴你這是 Zalo 訊息,但程式碼本身從不判斷平台。

啟動服務,看一條 Zalo 訊息進來

在 UnifyPort 工作空間裡連接一個 Zalo 帳號——Zalo 用掃碼認證,用 Zalo App 掃一下就連上了,幾秒鐘的事。不需要企業驗證,不需要註冊 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,不改一行 handler 程式碼

統一 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"
}

不需要新的 handler。不需要條件匯入。不需要 WhatsApp 專屬的解析邏輯。messages.jsonl 檔案裡現在同時包含 Zalo 和 WhatsApp 訊息,格式完全一樣,Slack 頻道裡兩個平台的訊息同步顯示。之後加 LINE 或 Telegram 也是一樣——連接帳號,已有的 handler 全部搞定。

重點不是 Claude Code 替你寫了伺服器。重點是,當 API 介面簡潔到可以完整放進 agent 上下文時,AI 程式碼工具就能一次做對。一種事件結構、一套簽章機制、一個傳送端點——這種目標,給了 prompt 的 agent 不會搞砸。把 UnifyPort API 參考文件餵給你的工具,給它 message.received 的 schema,午餐前你就能拿到一個簽章驗證、日誌記錄、訊息轉發的 webhook 接收器——然後不改程式碼就能接入其他所有頻道