Tôi đưa tài liệu API cho Claude Code và nó xây luôn một Zalo Webhook Receiver — UnifyPort
Zalo không có API tin nhắn công khai. Nếu bạn đang xây dựng sản phẩm cho thị trường Việt Nam — hoặc bất kỳ đội nhóm cross-border nào cần giao tiếp với khách hàng Việt Nam — đây là bức tường đầu tiên bạn đụng phải. Có con đường Official Account, nhưng đó là hệ thống broadcast: xác minh doanh nghiệp, template tin nhắn, cửa sổ tương tác, tính phí theo tin. Nếu bạn chỉ cần nhận tin nhắn mà khách hàng chủ động gửi đến, OA đang giải quyết một bài toán lớn hơn nhiều so với nhu cầu thực tế.
Nên tôi làm cách khác. Mở Claude Code, dán tài liệu webhook reference của UnifyPort vào context, yêu cầu nó viết một Zalo webhook receiver bằng Python. Năm mươi phút sau, tôi có một Flask server: xác minh chữ ký mỗi lần delivery, ghi tin nhắn Zalo vào file JSON có cấu trúc, đồng thời chuyển tiếp sang kênh Slack. Toàn bộ code khoảng 60 dòng.
Bạn sẽ có gì ở cuối bài
Một Python Flask server với các chức năng:
- Nhận event
message.receivedtừ webhook của UnifyPort - Xác minh chữ ký mỗi lần delivery bằng HMAC-SHA256 với
signing_secret - Ghi mỗi tin nhắn Zalo vào file JSON có cấu trúc
- Gửi tóm tắt sang kênh Slack qua incoming webhook
Thời gian: chưa đến một giờ, phần lớn là Claude Code làm. Bạn cần một workspace UnifyPort đã kết nối tài khoản Zalo (quét mã QR — không cần mật khẩu, không cần xác minh doanh nghiệp) và một Slack incoming webhook URL.
Chuẩn bị: đưa tài liệu vào context của Claude Code
Trước khi prompt, hãy đưa API reference vào context của agent. Claude Code đọc được file local, nên cách nhanh nhất là:
- Copy phần webhook event reference — đặc biệt là cấu trúc event
message.received, phần xác minh chữ ký, và endpointPOST /v1/webhook-endpoints— vào fileunifyport-reference.mdở thư mục gốc project. - Hoặc dán trực tiếp các phần liên quan vào cửa sổ chat khi bắt đầu.
Những gì Claude Code cần thấy: cấu trúc payload của message.received, header x-unifyport-signature, và cơ chế xác minh HMAC-SHA256 với signing_secret. Thiếu những thứ này, agent sẽ đoán tên field — và chắc chắn đoán sai.
Các tool khác cũng tương tự. Trong Cursor dùng @Docs trỏ vào reference. Trong Windsurf dùng docs panel. Logic giống nhau: đưa đúng tên field vào, code đúng ra.
Xây dựng từng bước, prompt từng prompt
Prompt đầu tiên — khung sườn:
Đọc unifyport-reference.md. Viết Flask server trong app.py với route POST /webhook. Khi field event là “message.received”, in provider, from, text ra stdout. Trả về 200 cho tất cả event.
Claude Code đọc xong reference và sinh ra:
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)
Bảy dòng xử lý. Vì cấu trúc event đã được chuẩn hóa, chỉ cần kiểm tra một field — không cần parse riêng cho từng nền tảng.
Prompt thứ hai — xác minh chữ ký:
Mỗi lần webhook delivery đều có header x-unifyport-signature. Xác minh bằng HMAC-SHA256 với signing_secret từ biến môi trường. HMAC phải tính trên raw request body, không phải JSON đã serialize lại. Trả về 401 nếu xác minh thất bại. Dùng so sánh constant-time.
Bước này biến đồ chơi thành thứ chạy được trên production. Claude Code implement đúng vì đã đọc reference:
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() trả về raw bytes trước khi Flask parse JSON — chi tiết này cực kỳ quan trọng. Nếu agent dùng request.get_json() rồi serialize lại, HMAC sẽ không bao giờ khớp. Prompt ghi “raw request body”, nó viết đúng ngay lần đầu.
Prompt thứ ba — log có cấu trúc và chuyển tiếp Slack:
Thêm hai thứ: (1) append mỗi event message.received vào file messages.jsonl, mỗi dòng một JSON object, gồm timestamp, provider, from, text. (2) POST một dòng tóm tắt sang Slack incoming webhook URL từ biến môi trường SLACK_WEBHOOK_URL.
Claude Code xử lý cả hai trong một lần. File app.py hoàn chỉnh:
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)
Sáu mươi dòng code, xác minh chữ ký, log có cấu trúc, chuyển tiếp Slack — đầy đủ. Toàn bộ handler không phụ thuộc nền tảng — evt["provider"] cho biết đây là tin Zalo, nhưng code không bao giờ phân nhánh theo nền tảng.
Chạy server và xem tin nhắn Zalo đến
Kết nối tài khoản Zalo trong workspace UnifyPort — Zalo dùng xác thực QR code, quét bằng app Zalo là kết nối trong vài giây. Không cần xác minh doanh nghiệp, không cần đăng ký OA, không cần quản lý mật khẩu. Đăng ký webhook endpoint với subscribed_events: ["message.received"] và signing_secret.
Khởi động server, rồi gửi một tin nhắn từ liên hệ Zalo. Event sẽ đến dạng như sau:
{
"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"
}
Server xác minh chữ ký, append record vào messages.jsonl, gửi tóm tắt sang Slack. Nếu xác minh chữ ký thất bại trả về 401 — kiểm tra UNIFYPORT_SIGNING_SECRET có khớp với cài đặt webhook endpoint không.
Mở rộng: thêm WhatsApp mà không đổi một dòng handler
Đây là lúc webhook chuẩn hóa thể hiện giá trị. Kết nối tài khoản WhatsApp trong cùng workspace, subscribe cùng webhook endpoint, route /webhook y hệt xử lý cả hai nền tảng. Tin nhắn WhatsApp đến cùng cấu trúc message.received — khác field provider, cấu trúc hoàn toàn giống:
{
"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"
}
Không cần handler mới. Không cần import có điều kiện. Không cần parse riêng cho WhatsApp. File messages.jsonl giờ chứa cả tin Zalo và WhatsApp cùng schema, kênh Slack hiển thị cả hai luồng. Thêm LINE hoặc Telegram sau cũng vậy — kết nối tài khoản, handler có sẵn xử lý hết.
Điều quan trọng không phải Claude Code viết server cho bạn. Điều quan trọng là khi API đủ đơn giản để nằm gọn trong context của agent, AI coding tool sẽ hoàn thành sạch sẽ ngay lần đầu. Một cấu trúc event, một cơ chế chữ ký, một endpoint gửi — target như vậy, agent được prompt đúng sẽ không sai. Đưa tài liệu UnifyPort API cho tool của bạn, đưa schema message.received, và bạn sẽ có webhook receiver hoàn chỉnh trước bữa trưa — rồi không cần sửa code để kết nối tất cả kênh khác.