Express Middleware ของ @line/bot-sdk ทำอะไร
middleware() จากแพ็กเกจทางการ @line/bot-sdk คือ handler แบบ Express สำหรับ route ที่รับ webhook จาก LINE มันอ่าน raw body ของ request ตรวจ header x-line-signature ด้วย channel secret ของคุณ (HMAC-SHA256) และถ้าลายเซ็นถูกต้องก็จะ parse JSON ใส่ req.body ให้ handler ของคุณใช้ req.body.events ได้ทันที ถ้าตรวจไม่ผ่าน มันจะส่ง error ต่อให้ next() และจะไม่ตอบ request เองเลย
การตรวจลายเซ็นใช้ได้กับไบต์ชุดเดียวกับที่ LINE ส่งมาเท่านั้น ถ้ามีอะไร parse body ไปก่อน เช่น express.json() ที่ตั้งไว้ทั้งแอป การตรวจจะล้มเหลว โค้ดในบทความนี้ใช้ SDK เวอร์ชันปัจจุบันคือ 11.3.0 (ณ เดือนตุลาคม 2026) ซึ่งต้องใช้ Node.js 22 ขึ้นไป เวอร์ชัน 11 ถอดคลาส Client แบบเดิมออกแล้ว โค้ดเก่าที่ใช้ new Client(config) กับ client.replyMessage(token, message) จึงคอมไพล์ไม่ผ่าน บทความนี้ใช้ LineBotClient แทน
| สิ่งที่ export | หน้าที่ |
|---|---|
middleware({ channelSecret }) | อ่าน raw body ตรวจลายเซ็น แล้ว parse JSON ใส่ req.body |
LineBotClient.fromChannelAccessToken() | client ของ Messaging API ที่มี type ครบ (reply, push, profile ฯลฯ) ส่วน messagingApi.MessagingApiClient ยังมีให้ใช้เช่นกัน |
webhook.CallbackRequest, webhook.Event | type ของ payload webhook และ event แต่ละแบบ |
validateSignature(body, secret, signature) | การตรวจแบบเดียวกันในรูปฟังก์ชัน สำหรับ Route Handler และ serverless |
SignatureValidationFailed, JSONParseError | error ที่ middleware ส่งต่อให้ next() |
ถ้ากำลังสร้างส่วนบทสนทนา ดูคู่มือเขียน LINE Bot ทีละขั้น ที่อธิบาย flow การสนทนาและการตอบด้วย AI ส่วนคู่มือเชื่อมต่อ LINE API อธิบาย LINE Login และการเรียก API โดยไม่ใช้ SDK
ติดตั้งและตั้งค่า (Node.js 22 ขึ้นไป)
คุณต้องมี Messaging API channel ก่อน ตั้งแต่เดือนกันยายน 2024 เป็นต้นมา สร้าง channel นี้ใน LINE Developers Console โดยตรงไม่ได้แล้ว ให้สร้าง LINE OA ก่อน แล้วเปิดใช้ Messaging API ใน LINE Official Account Manager (ขั้นตอนสร้างบัญชีอยู่ในคู่มือเปิด LINE OA) จากนั้นเตรียมโปรเจกต์:
mkdir line-webhook && cd line-webhook
npm init -y
npm pkg set type=module
npm install express @line/bot-sdk
npm install -D typescript tsx @types/express @types/node
เปิด channel ใน LINE Developers Console แล้วคัดลอก Channel secret (แท็บ Basic settings) และ channel access token (แท็บ Messaging API) LINE แนะนำ channel access token v2.1 ที่กำหนดวันหมดอายุเองได้ แต่ถ้าอยากเริ่มเร็วที่สุด ใช้ long-lived token ไปก่อนได้
# .env (ห้าม commit ไฟล์นี้)
LINE_CHANNEL_SECRET=your-channel-secret
LINE_CHANNEL_ACCESS_TOKEN=your-channel-access-token
PORT=3000
เพิ่มสคริปต์ dev ใน package.json:
{
"scripts": {
"dev": "tsx watch --env-file=.env src/index.ts"
}
}
ตัวอย่างที่ใช้งานได้จริงแบบสั้นที่สุด
นี่คือไฟล์ src/index.ts ทั้งไฟล์ของบอทที่ตอบข้อความกลับ (echo bot) คอมไพล์ผ่านกับ @line/bot-sdk 11.3.0 และ Express 5 ในโหมด TypeScript strict
// src/index.ts
import express from "express"
import {
JSONParseError,
LineBotClient,
SignatureValidationFailed,
middleware,
webhook,
} from "@line/bot-sdk"
const channelSecret = process.env.LINE_CHANNEL_SECRET
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN
if (!channelSecret || !channelAccessToken) {
throw new Error("Set LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN")
}
const client = LineBotClient.fromChannelAccessToken({ channelAccessToken })
const app = express()
// วาง middleware ของ LINE บน route webhook ก่อน JSON body parser ทุกตัว
app.post("/webhook", middleware({ channelSecret }), async (req, res) => {
const { events } = req.body as webhook.CallbackRequest
await Promise.all(events.map(handleEvent))
res.sendStatus(200)
})
// route อื่นใช้ JSON parser ได้ตามปกติ
app.use(express.json())
async function handleEvent(event: webhook.Event) {
if (event.type !== "message" || event.message.type !== "text" || !event.replyToken) return
const { sentMessages } = await client.replyMessage({
replyToken: event.replyToken,
messages: [{ type: "text", text: `Echo: ${event.message.text}` }],
})
console.log("sent message IDs:", sentMessages.map((m) => m.id))
}
// middleware ส่ง error ให้ next() เท่านั้น ไม่ได้ตอบ 401 เอง
app.use((err: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof SignatureValidationFailed) {
res.status(401).send("Invalid signature")
return
}
if (err instanceof JSONParseError) {
res.status(400).send("Invalid JSON")
return
}
next(err)
})
const port = Number(process.env.PORT ?? 3000)
app.listen(port, () => console.log(`LINE bot listening on :${port}`))
มี 4 จุดที่ทำให้โค้ดนี้ทำงานได้:
- middleware อยู่บน route webhook และมาก่อน JSON parser
express.json()ลงทะเบียนหลัง route นี้ จึงไม่แตะ/webhookเลย res.sendStatus(200)LINE ต้องการสถานะ 200 สำหรับ webhook ทุกครั้ง รวมถึง request ที่eventsเป็นอาร์เรย์ว่าง ซึ่ง LINE ส่งมาเพื่อเช็กการเชื่อมต่อ- error handler ถ้าไม่มี ลายเซ็นผิดจะจบที่ 500 ทั่วไปของ Express ถ้ามี คุณจะตอบ 401 หรือ 400 ได้และ log จะบอกสาเหตุชัดเจน
- การเช็ก
replyTokenใน type ของ v11 ค่าreplyTokenเป็น optional จึงต้องเช็กก่อนตอบ
ทดสอบโดยเปิดพอร์ต 3000 ผ่าน HTTPS ด้วย tunnel เช่น ngrok ใส่ https://<โดเมนของคุณ>/webhook เป็น Webhook URL ในแท็บ Messaging API กด Verify เปิด Use webhook แล้วลองส่งข้อความหาบัญชี LINE รับเฉพาะ Webhook URL ที่เป็น HTTPS และใช้ใบรับรองจากผู้ออกใบรับรองที่เบราว์เซอร์ทั่วไปเชื่อถือ ใบรับรองแบบ self-signed ใช้ไม่ได้ อย่าลืมปิด ข้อความทักทาย และ ข้อความตอบกลับอัตโนมัติ ใน LINE Official Account Manager (ค่าเริ่มต้นเปิดอยู่ทั้งคู่) ไม่อย่างนั้นผู้ใช้จะได้ทั้งข้อความอัตโนมัติของ LINE และข้อความจากบอทของคุณ
validateSignature ทำงานอย่างไร (Node, Python, PHP)
LINE เซ็นทุก webhook request โดยคำนวณ HMAC-SHA256 ของ request body ด้วย channel secret ของคุณ แล้วส่งผลลัพธ์ที่เข้ารหัส Base64 มาใน header x-line-signature (Verify webhook signature) ฟังก์ชัน validateSignature() ของ SDK คำนวณค่านี้ใหม่แล้วเทียบกันแบบใช้เวลาคงที่ (constant time) ถ้าเขียนด้วย Node.js ล้วน ๆ จะได้ประมาณนี้:
import { createHmac, timingSafeEqual } from "node:crypto"
// สิ่งที่ validateSignature() ทำ แบบย่อ
export function isValidSignature(body: string | Buffer, channelSecret: string, signature: string): boolean {
const expected = createHmac("sha256", channelSecret).update(body).digest()
const received = Buffer.from(signature, "base64")
return expected.length === received.length && timingSafeEqual(expected, received)
}
เวลาตรวจไม่ผ่าน สาเหตุแทบทั้งหมดอยู่ในรายการข้อผิดพลาดที่ LINE ระบุไว้:
- body ถูก parse หรือแปลงกลับเป็นสตริงก่อนตรวจ (
express.json()มาก่อน,JSON.stringify(req.body), จัดรูปแบบ JSON ใหม่) ต้องใช้ไบต์ดิบชุดเดิมเท่านั้น - proxy, load balancer หรือเฟรมเวิร์กแก้ body หรือ encoding ระหว่างทาง ให้คงเป็น UTF-8
- secret เป็นของ channel อื่น หรือมีคนออก channel secret ใหม่ใน console แล้ว
- ตัวแปร environment มีขึ้นบรรทัดใหม่หรือช่องว่างต่อท้าย
- ใช้อัลกอริทึมผิด เช่น HMAC-SHA1 แทน HMAC-SHA256
อย่าใช้การกรอง IP แทนการตรวจลายเซ็น เพราะ LINE ไม่เปิดเผย IP ที่ส่ง webhook และแนะนำให้ตรวจลายเซ็นแทน
ไม่ใช้ Express: Next.js Route Handler และ serverless
ถ้าไม่มี middleware chain ของ Express ให้เรียก validateSignature() เองกับข้อความดิบของ request:
// app/api/line/webhook/route.ts (Next.js App Router)
import { LineBotClient, validateSignature, webhook } from "@line/bot-sdk"
const channelSecret = process.env.LINE_CHANNEL_SECRET ?? ""
const client = LineBotClient.fromChannelAccessToken({
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN ?? "",
})
export async function POST(request: Request) {
const body = await request.text() // raw body: ตรวจก่อน JSON.parse เสมอ
const signature = request.headers.get("x-line-signature") ?? ""
if (!validateSignature(body, channelSecret, signature)) {
return new Response("Invalid signature", { status: 401 })
}
const { events } = JSON.parse(body) as webhook.CallbackRequest
await Promise.all(
events.map(async (event) => {
if (event.type !== "message" || event.message.type !== "text" || !event.replyToken) return
await client.replyMessage({
replyToken: event.replyToken,
messages: [{ type: "text", text: `Echo: ${event.message.text}` }],
})
}),
)
return new Response("OK")
}
บางแพลตฟอร์ม เช่น Google Cloud Functions และ Firebase จะ parse body ก่อนโค้ดของคุณทำงาน middleware จะใช้ req.rawBody ถ้ามีให้ แต่ถ้าคุณมีแค่ object ที่ parse แล้ว จะตรวจลายเซ็นให้แม่นยำไม่ได้ เพราะการแปลงกลับเป็นสตริงทำให้ไบต์เปลี่ยน
Python (line-bot-sdk v3)
SDK ของ Python (3.26.0, Python 3.10 ขึ้นไป) ตรวจแบบเดียวกันภายใน WebhookHandler.handle() ให้ใช้โมดูล linebot.v3 เพราะ API แบบเก่า from linebot import LineBotApi, WebhookHandler ถูกประกาศเลิกใช้แล้ว
import os
from flask import Flask, abort, request
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
ApiClient,
Configuration,
MessagingApi,
ReplyMessageRequest,
TextMessage,
)
from linebot.v3.webhooks import MessageEvent, TextMessageContent
app = Flask(__name__)
configuration = Configuration(access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"])
handler = WebhookHandler(os.environ["LINE_CHANNEL_SECRET"])
@app.post("/webhook")
def webhook():
signature = request.headers.get("X-Line-Signature", "")
body = request.get_data(as_text=True) # ใช้ raw body ไม่ใช่ request.json
try:
handler.handle(body, signature) # ตรวจลายเซ็นก่อน แล้วค่อยส่งต่อให้ handler
except InvalidSignatureError:
abort(400)
return "OK"
@handler.add(MessageEvent, message=TextMessageContent)
def handle_text(event):
with ApiClient(configuration) as api_client:
MessagingApi(api_client).reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text=f"Echo: {event.message.text}")],
)
)
ถ้าต้องการแค่ตรวจลายเซ็น ใช้ SignatureValidator(channel_secret).validate(body, signature) จาก linebot.v3 ซึ่งคืนค่า True หรือ False
PHP (line-bot-sdk-php)
ใน PHP SDK เวอร์ชันปัจจุบัน (v12, PHP 8.2 ขึ้นไป) เมธอดนี้เป็น static อยู่ที่ LINE\Parser\SignatureValidator คืนค่า true หรือ false และโยน InvalidSignatureException ถ้าลายเซ็นว่าง:
<?php
require __DIR__ . '/vendor/autoload.php';
use LINE\Parser\Exception\InvalidSignatureException;
use LINE\Parser\SignatureValidator;
$body = file_get_contents('php://input'); // raw request body
$signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
$channelSecret = (string) getenv('LINE_CHANNEL_SECRET');
try {
$isValid = SignatureValidator::validateSignature($body, $channelSecret, $signature);
} catch (InvalidSignatureException $e) {
$isValid = false; // ไม่มี header ลายเซ็น
}
if (!$isValid) {
http_response_code(400);
exit;
}
// ลายเซ็นถูกต้อง: decode $body แล้วจัดการ event ต่อ
รุ่นเก่ามีเมธอด $bot->validateSignature($body, $signature) อยู่ในคลาส \LINE\LINEBot บทความสอนหลายแห่งจึงยังใช้แบบนั้น แต่คลาสนี้ไม่มีในรุ่นปัจจุบันแล้ว ถ้าต้องการตรวจและ parse event ในครั้งเดียว ใช้ EventRequestParser::parseEventRequest($body, $channelSecret, $signature)
replyMessage(replyToken): ตัวอย่างเต็ม
webhook event ที่ตอบกลับได้จะมี replyToken มาด้วย กติกาตาม Messaging API reference มีดังนี้:
- reply token ใช้ได้ ครั้งเดียว
- ต้องใช้ ภายใน 1 นาที หลังได้รับ webhook ถ้าช้ากว่านั้นไม่รับประกันว่าจะใช้ได้ และ LINE แจ้งว่าเวลานี้อาจเปลี่ยนได้ จึงควรตอบให้เร็วที่สุด
- การ reply หนึ่งครั้งส่งได้ สูงสุด 5 message object
- ข้อความ reply ไม่นับรวมในโควตาข้อความรายเดือน แต่ push, multicast, broadcast และ narrowcast นับ
ตัวอย่าง reply ที่มีข้อความตัวอักษรหนึ่งข้อความและ Flex Message แบบ bubble:
import { LineBotClient, webhook } from "@line/bot-sdk"
const client = LineBotClient.fromChannelAccessToken({ channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN ?? "" })
export async function confirmBooking(event: webhook.MessageEvent) {
if (!event.replyToken) return
const { sentMessages } = await client.replyMessage({
replyToken: event.replyToken,
messages: [
{ type: "text", text: "ขอบคุณค่ะ ยืนยันการจองเรียบร้อยแล้ว" },
{
type: "flex",
altText: "ยืนยันการจอง: วันเสาร์ 14:00 น.",
contents: {
type: "bubble",
body: {
type: "box",
layout: "vertical",
contents: [
{ type: "text", text: "ยืนยันการจองแล้ว", weight: "bold", size: "lg" },
{ type: "text", text: "วันเสาร์ 14:00 น.", color: "#666666", wrap: true },
],
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
action: { type: "uri", label: "ดูรายละเอียด", uri: "https://example.com/bookings/123" },
},
],
},
},
},
],
notificationDisabled: false, // true = ส่งโดยไม่มี push notification
})
return sentMessages
}
ถ้าตั้ง notificationDisabled: true ข้อความจะเข้าโดยไม่มีการแจ้งเตือน ถ้าต้องส่งเกิน 5 ข้อความ หรือต้องตอบหลังจากเลย 1 นาทีไปแล้ว ให้ส่งส่วนที่เหลือด้วย pushMessage ซึ่งนับโควตา ถ้างานใช้เวลานาน เช่น เรียก AI ให้ใช้ client.showLoadingAnimation({ chatId: userId, loadingSeconds: 20 }) เพื่อแสดงแอนิเมชันกำลังโหลดในแชท 1:1 ได้ 5 ถึง 60 วินาที
ReplyMessageResponse.sentMessages และ quote token
replyMessage() คืนค่าเป็น ReplyMessageResponse ซึ่งมีอาร์เรย์ sentMessages หนึ่งรายการต่อหนึ่งข้อความที่ส่ง เรียงตามลำดับเดียวกับอาร์เรย์ messages ของคุณ:
type ReplyMessageResponse = {
sentMessages: Array<{
id: string // ID ของข้อความที่ส่ง
quoteToken?: string // มีเฉพาะประเภทข้อความที่อ้างอิง (quote) ได้
}>
}
pushMessage() คืนค่ารูปแบบเดียวกัน quoteToken จะมีเฉพาะข้อความที่ quote ได้ ได้แก่ text (v1 และ v2), sticker, image, video, template และ Flex Message (สองแบบหลังจะแสดงแค่ altText ในส่วนที่ถูก quote) quote token ไม่มีวันหมดอายุและใช้ซ้ำได้ แต่ใช้ได้เฉพาะในแชทที่ส่งข้อความนั้นเท่านั้น (Get quote tokens)
การ quote ทำได้โดยใส่ quoteToken ใน text message ข้อความตัวอักษรที่ลูกค้าส่งมาก็มี quoteToken ของตัวเอง บอทจึง quote ข้อความของลูกค้าตอนนี้ แล้วค่อย quote ข้อความยืนยันของตัวเองในภายหลังได้:
import { randomUUID } from "node:crypto"
import { LineBotClient, webhook } from "@line/bot-sdk"
const client = LineBotClient.fromChannelAccessToken({ channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN ?? "" })
export async function confirmOrder(event: webhook.MessageEvent, userId: string) {
if (event.message.type !== "text" || !event.replyToken) return
// quote ข้อความของลูกค้าในข้อความตอบกลับ
const { sentMessages } = await client.replyMessage({
replyToken: event.replyToken,
messages: [{ type: "text", text: "ยืนยันคำสั่งซื้อ #1042 แล้ว", quoteToken: event.message.quoteToken }],
})
const orderQuoteToken = sentMessages[0]?.quoteToken // เก็บไว้คู่กับคำสั่งซื้อ
// ภายหลัง (เช่น ตอนจัดส่ง): push message ที่ quote ข้อความยืนยัน
await client.pushMessage(
{ to: userId, messages: [{ type: "text", text: "จัดส่งสินค้าของคุณแล้ว", quoteToken: orderQuoteToken }] },
randomUUID(), // X-Line-Retry-Key: ส่งซ้ำด้วย key เดิมจะไม่ถูกส่งสองครั้ง
)
}
อาร์กิวเมนต์ตัวที่สองของ pushMessage() คือ X-Line-Retry-Key ถ้า request ที่ใช้ key เดียวกันเคยถูกรับไปแล้ว LINE จะตอบ 409 แทนการส่งข้อความซ้ำ การ retry หลัง timeout จึงปลอดภัย
Error ที่จะเจอจริง
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| webhook ทุกครั้งได้ 500 และปุ่ม Verify ใน console ไม่ผ่าน | SignatureValidationFailed ไปถึง error handler เริ่มต้นของ Express | เพิ่ม error handler ตามตัวอย่างข้างบน แล้วแก้ต้นเหตุ |
| 401 จาก error handler ของคุณ | body ถูก parse ก่อน middleware(), channel secret ผิดหรือถูกออกใหม่, proxy แก้ body | วาง middleware() เป็นตัวแรกบน /webhook ตรวจ secret และเก็บ raw body ไว้ |
| 400 จาก error handler ของคุณ | JSONParseError: body ไม่ใช่ JSON ที่ถูกต้อง | มักเกิดจาก request ทดสอบที่ทำเอง webhook จริงจาก LINE เป็น JSON เสมอ |
API ตอบ 400 Invalid reply token | token ถูกใช้แล้ว ใช้ช้าเกินไป หรือเป็นของ channel อื่น | ตอบครั้งเดียวและเร็ว ข้อความที่ส่งทีหลังให้ใช้ push |
API ตอบ 400 The request body has X error(s) | message object ไม่ถูกต้อง เช่น เกิน 5 ข้อความ ข้อความว่าง property ของ Flex ผิด โดย details[].property จะบอกฟิลด์ที่ผิด | แก้ฟิลด์นั้น และทดสอบ payload ด้วย endpoint กลุ่ม "Validate message objects" |
| API ตอบ 401 | ไม่มี channel access token หรือ token หมดอายุ/ถูกยกเลิก | ออก token ใหม่ |
| API ตอบ 429 | เกิน rate limit หรือ You have reached your monthly limit. (ข้อความฟรีหรือข้อความเพิ่มเติมหมด) | ลดความถี่ และตรวจโควตาของแพ็กเกจ |
| API ตอบ 413 | request ใหญ่เกิน 2 MB | ส่งข้อความให้น้อยลงหรือเล็กลง |
เมื่อเรียก API ไม่สำเร็จ SDK จะโยน HTTPFetchError ที่มี status, statusText, headers และ body:
import { HTTPFetchError, LineBotClient } from "@line/bot-sdk"
const client = LineBotClient.fromChannelAccessToken({ channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN ?? "" })
export async function safeReply(replyToken: string, text: string) {
try {
await client.replyMessage({ replyToken, messages: [{ type: "text", text }] })
} catch (err) {
if (err instanceof HTTPFetchError) {
// เช่น 400 {"message":"Invalid reply token"} หรือ 429 {"message":"You have reached your monthly limit."}
console.error("LINE API error", err.status, err.body)
return
}
throw err
}
}
ขอแก้ความเข้าใจผิดที่พบบ่อย: LINE ไม่ได้ ส่ง webhook ที่ล้มเหลวซ้ำโดยอัตโนมัติ Webhook redelivery เป็นตัวเลือกที่ต้องเปิดเองในแท็บ Messaging API เมื่อเปิดแล้ว LINE จะส่ง webhook ที่ไม่ได้รับคำตอบ 2xx ซ้ำตามจำนวนครั้งที่ LINE ไม่เปิดเผย (Receive messages)
สถาปัตยกรรม webhook สำหรับใช้งานจริง
ผู้ใช้ส่งข้อความ
│
▼
LINE Platform ── HTTPS POST + x-line-signature ──▶ POST /webhook (เซิร์ฟเวอร์ของคุณ)
│ middleware(): raw body → ตรวจ HMAC → JSON
│ ตอบ 200 OK ทันที
▼
handleEvent(event)
│ logic, ฐานข้อมูล, AI ของคุณ
▼
LINE Platform ◀── replyMessage / pushMessage ───────┘
│
▼
ผู้ใช้เห็นข้อความตอบกลับLINE แนะนำให้ประมวลผล event แบบ asynchronous เพื่อไม่ให้ request ที่ช้าหนึ่งตัวถ่วง request ถัดไป รูปแบบที่ใช้กันบ่อยคือตอบ 200 ก่อน แล้วค่อยจัดการ event เบื้องหลัง พร้อมข้าม event ซ้ำด้วย webhookEventId:
const seen = new Set<string>() // ใช้งานจริงให้เก็บใน Redis หรือฐานข้อมูล
app.post("/webhook", middleware({ channelSecret }), (req, res) => {
const { events } = req.body as webhook.CallbackRequest
res.sendStatus(200) // ตอบรับก่อน: LINE ต้องการแค่ 200
for (const event of events) {
if (seen.has(event.webhookEventId)) continue // event ซ้ำหรือถูกส่งซ้ำ
seen.add(event.webhookEventId)
handleEvent(event).catch((err) => console.error("event failed", event.webhookEventId, err))
}
})
ข้อควรจำ:
- event ซ้ำเกิดขึ้นได้ LINE อาจส่ง event เดิมมากกว่าหนึ่งครั้ง เช่น หลังปัญหาเครือข่ายหรือการ redelivery ค่า
webhookEventIdจะเหมือนเดิม และdeliveryContext.isRedeliveryบอกว่าเป็นการส่งซ้ำหรือไม่ เมื่อรันมากกว่าหนึ่ง instance ให้เก็บ ID ที่ประมวลผลแล้วใน Redis หรือฐานข้อมูลแทนหน่วยความจำ - ตอบก่อนก็มีราคาที่ต้องจ่าย เมื่อตอบ 200 ไปแล้ว LINE จะไม่ส่ง event นั้นซ้ำ ต้อง log ความล้มเหลวและ retry เอง
- แพลตฟอร์ม serverless อาจหยุดทำงานหลังส่ง response บน Vercel, AWS Lambda และแพลตฟอร์มคล้ายกัน งานที่ทำหลังส่ง response อาจไม่เสร็จ ให้ทำงานให้เสร็จก่อนตอบ (และยังต้องอยู่ในเวลา 1 นาทีของ reply token) หรือส่งงานต่อให้ API ที่ทำงานหลัง response เช่น
after()ของ Next.js หรือคิวงาน - หนึ่ง channel มี Webhook URL ได้ URL เดียว Messaging API channel ส่งทุก event ไปที่ URL เดียว จึงควรแยกตาม
event.typeภายในเซอร์วิสเดียว แทนที่จะให้หลายเซอร์วิสใช้ channel เดียวกัน - state ต้องเก็บฝั่งคุณ LINE ไม่เก็บสถานะบทสนทนาให้ ให้เก็บผูกกับ user ID ส่วนเมนูนำทางที่แสดงตลอดเวลาให้ใช้ Rich Menu
LineBot.pro เกี่ยวข้องตรงไหน
LineBot.pro ไม่ใช่ SDK ไม่ใช่ API และไม่ใช่แพลตฟอร์มโฮสติ้ง และไม่มี public API แต่มีสองส่วนที่ช่วยได้เมื่อคุณพัฒนาด้วยโค้ดข้างบน:
- ออกแบบข้อความและเมนู ตัวสร้างด้วย AI สร้าง Rich Message (Flex bubble หนึ่งชิ้นที่มีภาพ hero ข้อความ และปุ่ม) หรือ Rich Menu (ภาพพร้อม JSON ปุ่ม 1 ถึง 6 ปุ่ม) คุณคัดลอก JSON หรือดาวน์โหลดพร้อมรูปภาพ แล้วส่งด้วย
replyMessageหรือpushMessageหรืออัปโหลดเมนูเองได้ Rich Message ใช้ 8 เครดิต และ Rich Menu ใช้ 10 เครดิต บัญชีใหม่ได้เครดิตฟรี 50 เครดิต และซื้อเพิ่มเป็นแพ็กแบบจ่ายครั้งเดียว ไม่มีรายเดือน (ราคา) ลองตัวสร้าง Rich Message - ไม่ต้องเขียนโค้ดเลย ถ้าต้องการแค่ตอบตามคีย์เวิร์ด บอทตอบแชท LINE OA ตอบแชท 1:1 ด้วยการ์ด Rich Message ที่คุณอนุมัติไว้ มีตัวเลือก AI ช่วยจัดเส้นทางข้อความที่ไม่ตรงคีย์เวิร์ดใด โดย AI แค่เลือกการ์ด ไม่ได้เขียนคำตอบเอง บอทนี้ทำงานโดยนำ Webhook URL ของ LineBot.pro ไปใส่ใน channel ของคุณ จึงใช้ร่วมกับเซิร์ฟเวอร์ webhook ของคุณเองบน channel เดียวกันไม่ได้
ถ้าต้องการเชื่อมกับระบบจอง CRM หรือแอป LIFF เรารับพัฒนาระบบ LINE ตามความต้องการ
คำถามที่พบบ่อย
Q: ควรวาง middleware ของ @line/bot-sdk ไว้ตรงไหนใน Express?
วางบน route ของ webhook และให้อยู่ก่อน body parser ทุกตัว: app.post("/webhook", middleware({ channelSecret }), handler) ลงทะเบียน express.json() หลัง route นี้ หรือใช้เฉพาะ route ที่ต้องการ ถ้ามี parser ระดับแอปทำงานก่อน raw body จะหายไปและทุก request จะล้มเหลวด้วย SignatureValidationFailed
Q: ทำไม webhook ของ LINE ตอบ 500 แทนที่จะเป็น 401?
middleware ไม่ได้ตอบ response เองเมื่อตรวจไม่ผ่าน แต่เรียก next(err) ถ้าไม่มี error handler Express จะแปลงเป็น 500 ให้เพิ่ม handler ที่ตอบ 401 สำหรับ SignatureValidationFailed และ 400 สำหรับ JSONParseError
Q: ตรวจลายเซ็นด้วย line-bot-sdk-php อย่างไร?
เรียก LINE\Parser\SignatureValidator::validateSignature($body, $channelSecret, $signature) โดยใช้ raw body จาก php://input จะได้ค่า boolean และถ้าลายเซ็นว่างจะโยน InvalidSignatureException ส่วนเมธอด $bot->validateSignature() ในคลาส \LINE\LINEBot เป็นของ SDK รุ่นเก่า
Q: ตรวจลายเซ็นใน Python อย่างไร?
ใน line-bot-sdk v3 ใช้ WebhookHandler(channel_secret).handle(body, signature) ซึ่งตรวจลายเซ็นแล้วส่งต่อ event ให้ และโยน InvalidSignatureError ถ้าไม่ตรง ถ้าต้องการแค่ตรวจ ใช้ SignatureValidator(channel_secret).validate(body, signature) จาก linebot.v3 และต้องส่ง raw body ซึ่งใน Flask คือ request.get_data(as_text=True)
Q: ReplyMessageResponse.sentMessages มีข้อมูลอะไรบ้าง?
มี object หนึ่งชิ้นต่อหนึ่งข้อความที่ส่ง เรียงตามลำดับใน request ประกอบด้วย id ของข้อความ และ quoteToken สำหรับประเภทข้อความที่ quote ได้ เก็บ quote token ไว้ถ้าต้องการให้ข้อความถัดไป quote ข้อความนั้น
Q: ใช้ replyMessage กับ pushMessage ร่วมกันได้ไหม?
ได้ ตอบด้วย reply token ก่อน (ฟรี ภายในประมาณ 1 นาที สูงสุด 5 ข้อความ) แล้วส่งข้อความที่ตามมาทีหลังด้วย pushMessage ข้อความ push นับรวมในโควตารายเดือนของแพ็กเกจ โดยนับหนึ่งข้อความต่อผู้รับหนึ่งคน
ขั้นตอนต่อไป:
- สร้างบอทให้ครบด้วยคู่มือเขียน LINE Bot ด้วย Node.js และ Python
- เรียก API โดยไม่ใช้ SDK และเพิ่ม LINE Login: คู่มือเชื่อมต่อ LINE API
- ดูว่าอะไรเปลี่ยนไปสำหรับนักพัฒนา: อัปเดต LINE Messaging API 2026
- อยากให้เราพัฒนาให้: รับพัฒนาระบบ LINE
- ออกแบบ Flex Message ด้วย AI: ตัวสร้าง Rich Message



