ซ้อมรับมือ X OAuth ล่ม: ทีมเล็กเก็บ DM ไว้ในคิวขาเข้าที่มีลายเซ็นอย่างไร
วันที่ 1 กรกฎาคม หน้า status สำหรับนักพัฒนาของ X ให้สัญญาณที่มีประโยชน์กับทีมซัพพอร์ต: OAuth2.0 login และ /2/users/me ส่งกลับ 401 error ตั้งแต่ 23:00 UTC วันที่ 30 มิถุนายน ถึง 01:00 UTC วันที่ 1 กรกฎาคม เหตุการณ์ถูกแก้ไขแล้ว และหน้า status กลับมาแสดงว่าระบบทำงานปกติ สำหรับทีมที่เปิด X แค่วันละครั้ง นี่อาจเป็นเรื่องเล็ก แต่สำหรับทีมเล็กที่ workflow ซัพพอร์ตเริ่มจาก refresh OAuth, เรียก /2/users/me, แล้วค่อย polling activity นี่คือบทซ้อมรับมือเหตุการณ์จริง
กรณีนี้มาจากทีมเปิดตัวสินค้า 3 คนในสิงคโปร์ พวกเขารับคำถามลูกค้าจาก X, WhatsApp และ LINE ในช่วงเปิดตัวสินค้า โดยเฉพาะ LINE ที่สำคัญมากในไทยและญี่ปุ่น เหตุการณ์เดือนกรกฎาคมไม่ได้ทำให้ข้อความหาย แต่ postmortem ทำให้เห็นปัญหา: pipeline ซัพพอร์ตของทีมเอาการ refresh ตัวตน X ไปไว้เป็นก้าวแรกของทุก intake job ถ้าก้าวแรกนั้นตอบ 401 ส่วนที่เหลือของ job จะไม่เริ่มทำงานเลย
ทางแก้ไม่ใช่ “ไม่ใช้ X API อย่างเป็นทางการอีกต่อไป” แต่คือย้าย live customer intake ไปไว้ในคิวขาเข้าที่มีลายเซ็น เก็บทุก delivery ก่อน แล้วให้ API ของแต่ละแพลตฟอร์มเป็นชั้นสำหรับการตอบกลับหรือเติมข้อมูล ไม่ใช่ประตูที่ตัดสินว่าทีมจะเห็นข้อความหรือไม่
ก้าวแรกที่เปราะบาง
การเชื่อมต่อ X เวอร์ชันแรกของทีมเป็นรูปแบบปกติของปี 2026: ใช้ X API v2 กับ OAuth 2.0 PKCE ตรวจสอบ user ที่ authenticate แล้ว จากนั้นค่อยอ่าน direct message และ mention เอกสาร Direct Messages ของ X อธิบายว่า Manage Direct Messages คือ endpoint สำหรับสร้าง conversation, ส่ง DM และลบ DM event ในนามของ user ที่ authenticate แล้ว เงื่อนไขก็ชัดเจน: ต้องมี approved developer account, project และ app ใน Developer Console, และ user access token ผ่าน OAuth 2.0 PKCE
พื้นผิวทางการนี้เหมาะเมื่อโจทย์คือ “ส่ง DM นี้ผ่าน X” หรือ “จัดการ conversation ใน developer platform ของ X” แต่โจทย์ปฏิบัติการของทีมเปิดตัวต่างออกไป:
ลูกค้าส่งข้อความบน X, WhatsApp หรือ LINE
-> ระบบซัพพอร์ตได้รับข้อความ
-> event ถูกเก็บก่อน AI หรือมนุษย์เริ่มทำงาน
-> agent ตอบกลับจาก account ที่ถูกต้อง
job เก่ากลับลำดับนี้ มันขอให้ X ยืนยันตัวตนของ account ก่อนจะเอาอะไรเข้า support queue วันปกติไม่มีใครสังเกตเห็น แต่เมื่อ OAuth หรือ /2/users/me มีปัญหา queue จะไม่มี activity ใหม่จาก X เพราะ intake script ออกก่อนถึงขั้น routing
ทีมต้องการคุณสมบัติสองอย่างแทน: ข้อความควรมาถึงในรูป event และ queue ไม่ควรถูกกำหนดโดย identity endpoint ของ provider เดียว
สัญญาขาเข้าแบบใหม่
พวกเขาสร้าง UnifyPort webhook endpoint ก่อน:
curl -X POST https://api.unifyport.ai/v1/webhook-endpoints \
-H "X-Api-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://support.example.com/webhook",
"status": "active",
"subscribed_events": ["message.received"],
"signing_secret": "<WEBHOOK_SIGNING_SECRET>"
}'
unofficial interface ของ UnifyPort เชื่อมต่อบัญชีแชททั่วไปและส่ง activity ขาเข้าเป็น event stream มาตรฐาน สำหรับ X ค่า provider คือ twitter; สำหรับ WhatsApp และ LINE envelope เดียวกันยังใช้ได้ ข้อความ X DM จะเข้ามาแบบนี้:
{
"id": "evt_9b71a4c20d",
"type": "message.received",
"provider": "twitter",
"account_id": "acc_launch_x",
"occurred_at": "2026-07-09T02:30:00Z",
"data": {
"conversation": { "id": "x_dm_48192", "type": "user", "title": "Aria Chen" },
"sender": { "id": "x_user_48291", "type": "user", "name": "Aria Chen" },
"message": {
"id": "x_msg_20260709_001",
"type": "text",
"text": "The preorder link returns 401 for me. Can you check?",
"direction": "inbound",
"sent_at": "2026-07-09T02:29:58Z"
},
"event": { "kind": "message_received" }
}
}
field สำคัญตั้งใจให้เรียบง่าย: id, type, provider, account_id, occurred_at และ data support queue จึง route ตามค่าได้ ไม่ต้องเรียน event model ใหม่สำหรับทุกช่องทาง
ตรวจลายเซ็น เก็บ แล้วค่อย route
ทุก delivery มี X-Device-Timestamp และถ้าเปิด signing จะมี X-Device-Signature ด้วย ลายเซ็นคือ hex HMAC-SHA256 ของ timestamp, จุด และ raw request body โดยใช้ signing_secret ของ endpoint ทีมวาง verifier นี้ไว้หน้า queue:
import crypto from "crypto";
import express from "express";
const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const timestamp = req.get("X-Device-Timestamp") || "";
const signature = req.get("X-Device-Signature") || "";
const hmac = crypto.createHmac("sha256", signingSecret);
hmac.update(timestamp + ".");
hmac.update(req.body);
const expected = hmac.digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) {
res.status(401).end();
return;
}
const event = JSON.parse(req.body.toString("utf8"));
await storeEvent(event.id, req.body);
if (event.type === "message.received") {
await routeInboundMessage({
provider: event.provider,
accountId: event.account_id,
conversationId: event.data.conversation.id,
senderId: event.data.sender.id,
text: event.data.message.text,
occurredAt: event.occurred_at
});
}
res.status(200).end();
});
ลำดับสำคัญกว่า code เอง ตรวจลายเซ็นจาก raw bytes เก็บ event ตาม id แล้วค่อยส่งไป Slack, helpdesk, CRM หรือ AI triage เอกสารของ UnifyPort ระบุชัดว่า webhook event คือบันทึกเดียวของ inbound traffic; event ที่พลาดไปจะไม่มี message-history API ให้ดึงกลับทีหลัง ดังนั้นการเก็บ event เป็นส่วนหนึ่งของ intake edge ไม่ใช่ขั้นตอนเสริมภายหลัง
เกิดอะไรขึ้นในการซ้อมครั้งถัดไป
สองสัปดาห์ต่อมา ทีมทำ internal drill พวกเขาบล็อก job ที่เคยเรียก /2/users/me, ปล่อย webhook receiver ทำงานต่อ แล้วส่ง test message เข้าไปทั้งสามช่องทาง
ข้อความจาก X, WhatsApp และ LINE เข้าตาราง message.received เดียวกัน Slack alert ของ X ช้าลง เพราะทีมตั้งใจ pause worker ที่ดึง profile metadata แต่ raw event ถูกเก็บไว้แล้ว agent ยังเห็นว่าใครเขียนมา ข้อความมาถึงเมื่อไร account ไหนได้รับ และลูกค้าพูดว่าอะไร metadata เฉพาะ platform ค่อยตามมาทีหลังได้
การตอบกลับยังเป็น action ที่ชัดเจน เมื่อ agent ตัดสินใจตอบ backend เรียก POST /v1/messages พร้อม account ที่เชื่อมต่อและผู้รับ:
curl -X POST https://api.unifyport.ai/v1/messages \
-H "X-Api-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"account_id": "acc_launch_x",
"to": { "id": "x_user_48291", "type": "user" },
"message": {
"type": "text",
"text": "Thanks for flagging it. The checkout link is fixed now."
}
}'
ดีไซน์นี้ไม่ได้ทำให้ platform outage หายไป ถ้า X ใช้งานไม่ได้จริง ทุก integration ย่อมได้รับผลกระทบ ความต่างคือ practical กว่านั้น: support desk ไม่ต้องพึ่ง profile lookup หรือ polling job ให้สำเร็จก่อน ถึงจะเก็บ customer activity ที่ถูกส่งมาถึง webhook แล้วได้
Checklist ที่ทีมเก็บไว้
runbook ก่อน launch ถูกย่อเหลือห้าข้อ:
- สร้าง webhook ก่อน โดยใช้
subscribed_events: ["message.received"] - เปิด signing และตรวจ
X-Device-Signatureกับ raw body - เก็บทุก event ตาม
idก่อน routing, enrichment, AI หรือการ assign ให้มนุษย์ - มอง API ของ provider เป็นขั้น enrichment หรือ reply ไม่ใช่ประตู intake
- route ตาม
provider,account_idและdata.conversation.idเพื่อให้การเพิ่ม WhatsApp, LINE, Telegram, Zalo หรือ TikTok ไม่ต้องสร้าง queue ใหม่
เหตุการณ์ X เดือนกรกฎาคมกินเวลาไม่นาน นั่นทำให้มันเป็นสัญญาณทดสอบที่ดีมากกว่าภัยพิบัติ ทีมเล็กแทบไม่เคยลบ dependency ต่อ platform API ได้ทั้งหมด แต่เลือกตำแหน่งของ dependency ได้ วางมันไว้หลังคิวขาเข้าที่มีลายเซ็น ไม่ใช่ก่อนที่ข้อความลูกค้าจะไปถึงทีมของคุณ