← บทความทั้งหมด
บทช่วยสอน

ผมชี้ให้ 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 ให้ log from กับ 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 ลงไป แล้วก่อนหมดบ่ายคุณจะได้บอตที่ตรวจลายเซ็นและตอบกลับได้ — จากนั้นก็ปล่อยมันไปยังอีกห้าช่องทางที่เหลือ