Diễn tập sự cố X OAuth: cách một team nhỏ đưa DM vào hàng đợi inbound có chữ ký
Ngày 1/7, trang trạng thái dành cho developer của X đưa ra một tín hiệu rất đáng chú ý cho các team hỗ trợ: OAuth2.0 login và /2/users/me trả lỗi 401 từ 23:00 UTC ngày 30/6 đến 01:00 UTC ngày 1/7. Sự cố đã được xử lý, và trang trạng thái sau đó hiển thị mọi hệ thống hoạt động bình thường. Với team chỉ thỉnh thoảng mở X, đây chỉ là tiếng ồn nền. Với team nhỏ có quy trình hỗ trợ bắt đầu bằng việc refresh OAuth, gọi /2/users/me, rồi polling activity, đây là một bài diễn tập sự cố.
Câu chuyện này nói về một team 3 người ở Singapore, vận hành hỗ trợ qua X, WhatsApp và LINE trong các đợt ra mắt sản phẩm. Họ không mất tin nhắn vì sự cố tháng 7. Điều hữu ích đến từ phần postmortem: chính pipeline hỗ trợ của họ đã đặt bước refresh danh tính X làm bước đầu tiên trong mọi job intake. Khi dependency ở bước đầu trả 401, phần còn lại của job không bao giờ chạy.
Cách sửa không phải là “không bao giờ dùng X API chính thức”. Cách sửa là đưa live customer intake vào một hàng đợi inbound có chữ ký, lưu mọi delivery trước, rồi xem API của từng nền tảng là lớp trả lời hoặc bổ sung dữ liệu, không phải cổng quyết định team có nhìn thấy tin nhắn hay không.
Bước đầu tiên quá mong manh
Tích hợp X đầu tiên của team khá bình thường trong năm 2026. Nó dùng X API v2 với OAuth 2.0 PKCE, kiểm tra user đã xác thực, rồi truy vấn bề mặt direct message và mention. Tài liệu Direct Messages của X mô tả Manage Direct Messages là các endpoint để tạo conversation, gửi DM và xóa DM event thay mặt user đã xác thực. Điều kiện cũng rõ: approved developer account, project và app trong Developer Console, cùng user access token qua OAuth 2.0 PKCE.
Bề mặt chính thức này hợp lý khi nhiệm vụ là “gửi DM này qua X” hoặc “quản lý conversation trong developer platform của X”. Nhưng nhiệm vụ vận hành của team ra mắt lại khác:
Khách hàng nhắn trên X, WhatsApp hoặc LINE
-> hệ thống hỗ trợ nhận tin nhắn
-> event được lưu trước khi AI hoặc con người xử lý
-> agent trả lời từ đúng account
Job cũ đảo ngược thứ tự đó. Nó yêu cầu X chứng minh danh tính account trước khi đưa bất kỳ thứ gì vào hàng đợi hỗ trợ. Ngày bình thường thì không ai chú ý. Khi OAuth hoặc /2/users/me gặp sự cố, queue không có activity X mới vì script intake thoát trước bước routing.
Team cần hai thuộc tính khác: tin nhắn phải đến dưới dạng event, và queue không nên được định hình quanh identity endpoint của một provider.
Hợp đồng inbound mới
Họ đăng ký webhook endpoint trong UnifyPort trước:
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>"
}'
Giao diện không chính thức của UnifyPort kết nối các account nhắn tin thông thường và gửi activity inbound thành một event stream chuẩn. Với X, giá trị provider là twitter; với WhatsApp, Zalo và LINE, cùng envelope đó vẫn áp dụng. Một X DM đến như sau:
{
"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" }
}
}
Các field quan trọng cố tình rất đơn giản: id, type, provider, account_id, occurred_at và data. Support queue có thể route theo value thay vì học một event model mới cho mỗi kênh.
Xác minh, lưu, rồi mới route
Mỗi delivery có X-Device-Timestamp; nếu bật signing thì có thêm X-Device-Signature. Chữ ký là hex HMAC-SHA256 của timestamp, dấu chấm và raw request body, ký bằng signing_secret của endpoint. Team đặt lớp verifier này trước 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();
});
Thứ tự quan trọng hơn bản thân đoạn code. Xác minh chữ ký bằng raw bytes. Lưu event theo id. Sau đó mới gửi đến Slack, helpdesk, CRM hoặc job AI triage. Tài liệu UnifyPort nói rõ webhook event là bản ghi duy nhất của inbound traffic; event bị bỏ lỡ không thể được backfill từ message-history API sau này. Vì vậy lưu trữ là một phần của intake edge, không phải bước phụ ở downstream.
Điều gì xảy ra trong lần diễn tập tiếp theo
Hai tuần sau, team chạy một bài diễn tập nội bộ. Họ chặn job từng gọi /2/users/me, giữ webhook receiver online, rồi gửi test message vào cả ba kênh.
Tin nhắn X, WhatsApp và LINE đều vào cùng bảng message.received. Alert Slack cho X chậm hơn vì team cố ý pause worker lấy profile metadata, nhưng raw event đã được lưu. Agent vẫn nhìn thấy ai nhắn, tin đến lúc nào, account nào nhận, và khách nói gì. Metadata riêng của provider có thể cập nhật sau.
Reply vẫn là hành động rõ ràng. Khi agent quyết định trả lời, backend dùng account đã kết nối và người nhận để gọi POST /v1/messages:
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."
}
}'
Thiết kế này không làm sự cố nền tảng biến mất. Nếu chính X không khả dụng, mọi tích hợp đều bị ảnh hưởng. Điểm khác biệt nhỏ hơn nhưng thực tế hơn: support desk không còn phụ thuộc vào profile lookup hoặc polling job thành công trước khi lưu activity của khách hàng đã được gửi tới webhook.
Checklist họ giữ lại
Runbook trước mỗi launch được rút gọn thành năm dòng:
- Đăng ký webhook trước, dùng
subscribed_events: ["message.received"]. - Bật signing và xác minh
X-Device-Signaturebằng raw body. - Lưu mọi event theo
idtrước routing, enrichment, AI hoặc phân công người xử lý. - Xem API của provider là bước enrichment hoặc reply, không phải cổng intake.
- Route theo
provider,account_idvàdata.conversation.id, để thêm WhatsApp, LINE, Telegram, Zalo hoặc TikTok không tạo thêm một queue mới.
Sự cố X tháng 7 khá ngắn. Chính vì vậy nó là một tín hiệu kiểm thử tốt hơn là một thảm họa. Team nhỏ hiếm khi loại bỏ được mọi dependency vào API nền tảng. Nhưng họ có thể chọn dependency đó đứng ở đâu. Hãy đặt nó sau hàng đợi inbound có chữ ký, không phải trước khi tin nhắn của khách hàng đến được với team.