ผมชี้ให้ AI coding agent อ่านเอกสาร webhook แล้วมันสร้างบอตตอบกลับอัตโนมัติที่ใช้งานได้จริงให้เลย — UnifyPort
คุณคุ้นกับจังหวะนี้ดี ในลิสต์งานมีงานเชื่อมต่อระบบแชตค้างอยู่ แทนที่จะอ่านเอกสารตั้งแต่ต้นจนจบ คุณเปิด Cursor — หรือ Claude Code หรือ Windsurf — แล้วเริ่มพิมพ์พรอมป์ ปี 2026 คนส่วนใหญ่เริ่มโปรเจกต์กันแบบนี้ คำถามที่แท้จริงไม่ใช่ว่าคุณจะ “vibe-code” กับ AI หรือไม่ แต่อยู่ที่ว่า API ที่คุณเชื่อมด้วยนั้นให้ข้อมูลพอให้เอเจนต์ทำงานจน เรียบร้อย ได้หรือเปล่า
นี่คือบันทึกการสร้างจริง อ่านจบคุณจะได้บอตตอบกลับอัตโนมัติที่รันได้: ตัวรับ webhook ที่ตรวจลายเซ็นกับทุกข้อความเข้า แล้วตอบกลับผ่าน endpoint ส่งเพียงตัวเดียว — งานราวบ่ายเดียว และส่วนใหญ่เป็นฝีมือเอเจนต์ ที่เร็วได้เพราะ webhook ของ UnifyPort ถูกทำให้เป็นมาตรฐานแล้ว มีโครงสร้างอีเวนต์แบบเดียวที่ต้องเรียนรู้ และ endpoint เดียวที่ต้องเรียกกลับ เอเจนต์จึงไม่มีอะไรต้องเดา
เตรียมตัว: ชี้เอกสารให้เอเจนต์ก่อน
ขั้นที่สำคัญที่สุดกลับเป็นขั้นที่คนชอบข้าม — เอาเอกสารอ้างอิง API เข้าไปในบริบทของเอเจนต์ ก่อน ที่คุณจะขอโค้ด วันนี้มีสามวิธีที่ได้ผล:
- วางหน้าเอกสารลงไป คัดลอกโครงสร้างอีเวนต์
message.receivedและส่วนส่งPOST /v1/messagesวางตรง ๆ ลงในแชต - แนบเอกสาร
@Docsของ Cursor, บริบทไฟล์ของ Claude Code, แผงเอกสารของ Windsurf — ชี้ไปที่เอกสารอ้างอิงของ UnifyPort ให้เอเจนต์อ่านเมื่อต้องการ - ใช้ตัวเชื่อมเอกสาร/MCP ถ้าเครื่องมือของคุณรองรับ ให้เอเจนต์ไปดึงเอกสารเอง
ผลตอบแทนเป็นรูปธรรม เอเจนต์ที่ได้อ่านชื่ออีเวนต์จริง ๆ จะไม่กุ handler ชื่อ onMessage() หรือ SDK ชื่อ sendText() ที่ไม่มีอยู่จริงขึ้นมา มันจะเขียนตาม message.received และ POST /v1/messages เพราะนั่นคือสิ่งที่อยู่ตรงหน้า ป้อนบริบทขยะเข้าไป ก็ได้ endpoint หลอน ๆ ออกมา — ฉะนั้นเสียเวลาสองนาทีเอาของจริงใส่เข้าไป
สร้างบอต ทีละพรอมป์
พรอมป์แรก — ตัวรับ:
ใช้เอกสาร webhook ของ UnifyPort ที่ผมให้ เขียนเซิร์ฟเวอร์ Express ที่มี route
POST /webhookรับอีเวนต์มาตรฐาน เอาแค่: เมื่อeventเป็นmessage.receivedให้ logfromกับtext
เพราะโครงสร้างอีเวนต์ตายตัว ตัวจัดการจึงเป็นแค่การแยกสาขาตามฟิลด์เดียว:
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"));
พรอมป์ที่สอง — ตรวจลายเซ็น ขั้นนี้คนสร้างแบบลวก ๆ จะข้าม แต่เอเจนต์ที่อ่านเอกสารแล้วจะไม่ข้าม:
ทุกการส่งถูกเซ็นด้วย HMAC-SHA256 โดยใช้
signing_secretของ webhook ให้ตรวจสอบก่อนจะเชื่อ body ใช้ raw request body และเปรียบเทียบแบบ timing-safe
มีจุดหนึ่งที่เอเจนต์ต้องทำให้ถูก: HMAC ต้องคำนวณบน ไบต์ดิบ ไม่ใช่ JSON ที่ถูก serialize ใหม่ ถ้าพรอมป์บอกว่า “ใช้ raw request body” เอเจนต์ที่ดีจะต่อ hook verify ของ express.json เพื่อเก็บ rawBody ถ้ามันลืม นั่นคือจุดเดียวที่คุณต้องคอยตรวจ — และคุณจะเจอทันทีที่การส่งจริงครั้งแรกตอบกลับ 401
พรอมป์ที่สาม — ตอบกลับผ่าน endpoint ส่ง:
เมื่อ
message.receivedมาถึง ให้ตอบผู้ส่งผ่านPOST /v1/messagesโดยใช้account_idกับfromจากอีเวนต์ การยืนยันตัวตนใช้ Bearer API key จาก environment
บอตก็ครบแล้ว นี่คือไฟล์เต็มที่เอเจนต์ลงตัว:
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: "ขอบคุณค่ะ! เราได้รับข้อความแล้ว เดี๋ยวเจ้าหน้าที่จะติดต่อกลับนะคะ",
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
รันแล้วดูข้อความวิ่งเข้ามา
เชื่อมบัญชีหนึ่งบัญชี (ในไทยที่ LINE เป็นช่องทางหลัก การเชื่อม LINE ต่อยอดใช้งานจริงได้ทันที), ลงทะเบียน endpoint webhook ที่ชี้มายังเซิร์ฟเวอร์ของคุณ พร้อม signing_secret และ subscribed_events: ["message.received"] แล้วส่ง DM ไปหาบอต อีเวนต์จะมาถึงหน้าตาแบบนี้:
{
"event": "message.received",
"account_id": "acct_8Q2vK",
"provider": "line",
"from": "user_3f9c1a",
"text": "ร้านเปิดวันเสาร์อาทิตย์ไหมคะ?",
"timestamp": 1749427200,
"message_id": "line_msg_5d2b7e"
}
เซิร์ฟเวอร์ของคุณตรวจลายเซ็น เรียก POST /v1/messages แล้วคำตอบก็โผล่กลับเข้าไปในแชต ถ้าการตรวจลายเซ็นไม่ผ่าน คุณจะได้ 401 — นั่นคือบั๊ก rawBody ข้างบน แก้ครั้งเดียวแล้ววงจรนี้ก็แน่น
ต่อยอด
นี่คือจุดที่ webhook แบบมาตรฐานคุ้มค่า การเพิ่มแพลตฟอร์มที่สองไม่ใช่การเชื่อมต่ออีกรอบ — แต่เป็นโค้ดตัวจัดการใหม่ ศูนย์ บรรทัด นอกจาก LINE ที่เป็นช่องทางหลักในไทย คุณจะเชื่อมบัญชี WhatsApp หรือ Zalo เพิ่ม สมัครรับ webhook เดียวกัน แล้วสาขา message.received เดิมเป๊ะก็ทำงาน เพราะโครงสร้างอีเวนต์ไม่เปลี่ยนข้ามผู้ให้บริการ ฟิลด์ provider บอกคุณว่ามาจากไหน ส่วนโค้ดที่เหลือไม่ต้องไปสนเลย
อยากให้บอตตอบคำถามจริง ๆ แทนที่จะแค่ตอบรับ? เพิ่มอีกพรอมป์เดียว: “ก่อนตอบ ให้ส่ง text ไปยัง LLM แล้วใช้คำตอบของมันเป็นเนื้อหาที่ตอบกลับ” การเรียกส่งเอเจนต์เขียนไว้แล้ว คุณแค่เปลี่ยนสตริงหนึ่งให้เป็นผลลัพธ์ของโมเดล
บทเรียนของบ่ายนี้ไม่ใช่ “AI เขียนบอตให้คุณ” แต่คือ: AI เอเจนต์ดีได้แค่เท่ากับหน้าอินเทอร์เฟซที่คุณชี้ให้มัน webhook แบบมาตรฐานที่มีชื่ออีเวนต์คงที่และ endpoint ส่งเพียงตัวเดียว คือหน้าอินเทอร์เฟซแบบที่เอเจนต์เก็บงานได้เรียบร้อย — มีโครงสร้างเดียวให้เรียนรู้ ไม่มีอะไรให้กุ ชี้เครื่องมือของคุณไปที่ UnifyPort v1 API วางโครงสร้าง message.received ลงไป แล้วก่อนหมดบ่ายคุณจะได้บอตที่ตรวจลายเซ็นและตอบกลับได้ — จากนั้นก็ปล่อยมันไปยังอีกห้าช่องทางที่เหลือ