Tôi chỉ cho một AI coding agent đọc tài liệu webhook, và nó dựng ra một bot tự trả lời chạy được — UnifyPort
Bạn quen cảnh này rồi. Trong danh sách việc có một tác vụ tích hợp nhắn tin, và thay vì đọc tài liệu từ đầu đến cuối, bạn mở Cursor — hoặc Claude Code, hoặc Windsurf — rồi bắt đầu gõ prompt. Năm 2026, phần lớn chúng ta khởi động dự án theo cách đó. Câu hỏi thật sự không phải bạn có “vibe-code” cùng AI hay không. Mà là cái API bạn đang làm việc cùng có cho agent đủ thứ để hoàn thành gọn gàng hay không.
Đây là một build log. Đọc xong, bạn sẽ có một bot tự trả lời chạy được: một receiver webhook kiểm tra chữ ký trên mỗi tin nhắn đến và trả lời qua một endpoint gửi duy nhất — khoảng một buổi chiều, và phần lớn là agent làm. Nó nhanh vì webhook của UnifyPort đã được chuẩn hóa. Chỉ một hình dạng sự kiện cần học, một endpoint cần gọi lại — agent chẳng phải đoán gì.
Chuẩn bị: chỉ cho agent đọc tài liệu
Bước quan trọng nhất lại là bước người ta hay bỏ qua: đưa tài liệu API vào ngữ cảnh của agent trước khi bạn yêu cầu code. Ba cách hiệu quả hiện nay:
- Dán trang tài liệu. Sao chép hình dạng sự kiện
message.receivedvà phần gửiPOST /v1/messagesthẳng vào khung chat. - Đính kèm tài liệu.
@Docscủa Cursor, ngữ cảnh tệp của Claude Code, bảng tài liệu của Windsurf — trỏ tới tài liệu UnifyPort để agent đọc khi cần. - Dùng connector tài liệu/MCP nếu công cụ của bạn hỗ trợ, để agent tự kéo tài liệu về.
Phần thưởng rất cụ thể. Một agent thật sự đã đọc tên sự kiện sẽ không bịa ra handler onMessage() hay một SDK sendText() chẳng tồn tại. Nó viết theo message.received và POST /v1/messages, vì đó là thứ đang ở trước mặt nó. Ngữ cảnh rác đưa vào, endpoint ảo giác đưa ra — nên hãy bỏ ra hai phút để đưa đúng thứ thật vào.
Dựng bot, từng prompt một
Prompt đầu — receiver:
Dùng tài liệu webhook UnifyPort tôi đã đưa, viết một server Express với route
POST /webhooknhận các sự kiện chuẩn. Trước mắt: khieventlàmessage.received, logfromvàtext.
Vì hình dạng sự kiện cố định, phần xử lý chỉ là một nhánh rẽ trên một trường:
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
const evt = req.body;
if (evt.event === "message.received") {
console.log(`${evt.provider} ${evt.from}: ${evt.text}`);
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
Prompt thứ hai — kiểm tra chữ ký. Đây là bước một bản dựng cẩu thả bỏ qua, còn agent đã đọc tài liệu thì không:
Mỗi lần gửi được ký bằng HMAC-SHA256 với
signing_secretcủa webhook. Hãy kiểm tra trước khi tin vào body. Dùng raw request body và so sánh an toàn về thời gian.
Có một điều agent buộc phải làm đúng ở đây: HMAC phải tính trên các byte thô, không phải JSON đã được tuần tự hóa lại. Khi được nhắc “dùng raw request body”, một agent tốt sẽ nối hook verify của express.json để bắt rawBody. Nếu nó quên, đó là điểm rà soát duy nhất của bạn — và bạn sẽ thấy ngay lần gửi thật đầu tiên trả về 401.
Prompt thứ ba — trả lời qua endpoint gửi:
Khi
message.receivedđến, trả lời người gửi quaPOST /v1/messagesbằngaccount_idvàfromlấy từ sự kiện. Xác thực dùng Bearer API key trong biến môi trường.
Vậy là xong con bot. Đây là tệp hoàn chỉnh agent dừng lại ở:
import express from "express";
import crypto from "crypto";
const { UNIFYPORT_API_KEY, UNIFYPORT_SIGNING_SECRET } = process.env;
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", UNIFYPORT_SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post("/webhook", async (req, res) => {
if (!isValid(req)) return res.sendStatus(401);
const evt = req.body;
if (evt.event === "message.received") {
await fetch("https://api.unifyport.ai/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${UNIFYPORT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
account_id: evt.account_id,
to: evt.from,
text: "Cảm ơn bạn! Shop đã nhận được tin nhắn, nhân viên sẽ phản hồi ngay.",
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
Chạy thử và xem một tin nhắn đến
Kết nối một tài khoản (Telegram nhanh nhất để thử), đăng ký một endpoint webhook trỏ về server của bạn với signing_secret và subscribed_events: ["message.received"], rồi gửi cho bot một tin nhắn riêng. Sự kiện đến trông như sau:
{
"event": "message.received",
"account_id": "acct_8Q2vK",
"provider": "telegram",
"from": "user_3f9c1a",
"text": "Cho hỏi cuối tuần shop có mở không ạ?",
"timestamp": 1749427200,
"message_id": "tg_msg_5d2b7e"
}
Server kiểm tra chữ ký, gọi POST /v1/messages, và lời trả lời xuất hiện lại trong cuộc trò chuyện. Nếu kiểm tra chữ ký thất bại, bạn nhận 401 — chính là lỗi rawBody ở trên. Sửa một lần là vòng lặp vững.
Mở rộng
Đây là chỗ webhook chuẩn hóa hoàn vốn. Thêm nền tảng thứ hai không phải là một lần tích hợp nữa — mà là không thêm dòng code handler nào. Với các đội ở Việt Nam vốn chạy song song cả WhatsApp lẫn Zalo, điều này rất đáng giá: kết nối thêm một tài khoản WhatsApp hay Zalo (hoặc LINE), đăng ký cùng một webhook, và đúng nhánh message.received ấy chạy, vì hình dạng sự kiện không đổi giữa các nhà cung cấp. Trường provider cho biết tin đến từ đâu, còn phần code còn lại của bạn thậm chí không cần ngó tới.
Muốn bot thật sự trả lời thay vì chỉ báo đã nhận? Thêm một prompt nữa thôi: “trước khi trả lời, gửi text cho một LLM và dùng câu trả lời của nó làm nội dung trả lời.” Lời gọi gửi agent đã viết rồi — bạn chỉ đổi một chuỗi thành đầu ra của mô hình.
Bài học của buổi chiều này không phải “AI viết con bot hộ bạn”. Mà là: một AI agent chỉ tốt ngang với bề mặt bạn trỏ nó vào. Một webhook chuẩn hóa với tên sự kiện ổn định và một endpoint gửi chính là loại bề mặt agent hoàn thành gọn gàng — một hình dạng để học, không có gì để bịa. Trỏ công cụ của bạn vào UnifyPort v1 API, dán hình dạng message.received vào, và trước khi hết chiều bạn sẽ có một con bot biết kiểm tra chữ ký và trả lời — rồi thả nó sang năm kênh còn lại.