เชื่อมต่อ LINE API สำหรับนักพัฒนา: Messaging API, Login, LIFF
ภาพรวม LINE API ปี 2026 สำหรับนักพัฒนา และวิธีเรียกใช้โดยตรง: แต่ละ API ทำอะไรได้ การตั้งค่า channel การ reply และ push ด้วย fetch การตรวจ signature ของ webhook, LINE Login แบบ PKCE, ID token จาก LIFF, rate limit และการจัดการ error

#LINE API ทำอะไรได้บ้าง (2026)
LINE เปิดให้นักพัฒนาใช้งานส่วนประกอบหลัก 4 อย่าง ได้แก่ Messaging API สำหรับรับส่งข้อความในนามของ LINE Official Account, LINE Login สำหรับให้ผู้ใช้เข้าสู่ระบบเว็บไซต์หรือแอปด้วยบัญชี LINE, LIFF (LINE Front-end Framework) สำหรับเปิดเว็บแอปของคุณภายใน LINE และแพลตฟอร์ม LINE MINI App ที่นำแอปซึ่งสร้างด้วย LIFF ไปแสดงเป็นบริการใน LINE บทความนี้สอนเรียก API เหล่านี้ตรงผ่าน HTTPS ด้วย fetch บน Node.js 22 โดยไม่ใช้ SDK พร้อม webhook เวอร์ชัน Python ข้อมูลทั้งหมดตรวจกับ developers.line.biz ณ เดือนตุลาคม 2026
| API | ทำอะไรได้ | ตัวอย่างการใช้งาน |
|---|---|---|
| Messaging API | ตอบกลับข้อความ ส่ง push, multicast, broadcast และ narrowcast จัดการ Rich Menu, quick reply, Flex Message ดึงโปรไฟล์ผู้ใช้ audience สถิติ และคูปอง | แชทบอท แจ้งเตือนคำสั่งซื้อและการจอง งานบริการลูกค้า |
| LINE Login | ล็อกอินแบบ OAuth 2.0 / OpenID Connect ได้ user ID ชื่อที่แสดงและรูปโปรไฟล์ ได้อีเมลเมื่อ LINE อนุมัติสิทธิ์แล้ว และชวนผู้ใช้เพิ่ม LINE OA เป็นเพื่อนได้ | ปุ่ม "เข้าสู่ระบบด้วย LINE" บนเว็บหรือแอป การผูกบัญชีสมาชิก |
| LIFF | เว็บแอปที่เปิดใน LINE และรู้ว่าผู้ใช้เป็นใคร (ID token, โปรไฟล์) ส่งข้อความเข้าห้องแชทที่เปิดอยู่ เปิด share target picker สแกน QR code | แบบฟอร์ม บัตรสมาชิก หน้าจองคิวภายใน LINE |
| LINE MINI App | แอป LIFF ที่ LINE แสดงเป็นบริการ ตั้งแต่ 11 มีนาคม 2026 ใครก็เผยแพร่ MINI App แบบ unverified ในไทยและไต้หวันได้ | แอปร้านค้า การจอง ระบบสมาชิก โดยไม่ต้องทำแอป native |
| LINE Social Plugins | ปุ่มแชร์และปุ่มเพิ่มเพื่อนสำหรับเว็บไซต์ | แชร์บทความ เพิ่มเพื่อนจากหน้าเว็บ |
API บางตัวเปิดให้เฉพาะลูกค้าองค์กรที่ได้รับอนุมัติ (เช่น LINE notification messages) และมีบริการ 2 อย่างที่ยังเห็นในบทความเก่าแต่เลิกให้บริการแล้ว คือ LINE Notify ปิดตัวเมื่อ 31 มีนาคม 2025 (ให้ส่ง push message ผ่าน Messaging API จาก LINE OA แทน อ่านทางเลือกแทน LINE Notify) และ LINE ได้ถอดเอกสาร LINE Pay ออกเมื่อเมษายน 2025 หลังจาก LINE Pay ในญี่ปุ่นยุติบริการ
ควรเลือก API ไหน
- บอทตอบลูกค้าหรือส่งการแจ้งเตือน → Messaging API
- ระบบล็อกอินบนเว็บไซต์หรือแอปของคุณเอง → LINE Login
- หน้าเว็บที่เปิดใน LINE และรู้ว่าผู้ใช้คือใคร → LIFF (ตอนนี้ LINE แนะนำให้สร้าง LIFF app ใหม่เป็น LINE MINI App)
- ทั้งแชทและหน้าเว็บ → ใช้ Messaging API คู่กับ LIFF ภายใต้ provider เดียวกัน
ถ้าอยากใช้ SDK ทางการ ดูตัวอย่าง @line/bot-sdk Express middleware และสอนเขียน LINE Bot ด้วย Node.js และ Python ซึ่งสร้าง webhook เดียวกันด้วย @line/bot-sdk v11
#ตั้งค่าเริ่มต้น: Provider, Channel และ Credentials
1. บัญชีนักพัฒนาและ provider เข้าสู่ระบบ LINE Developers Console แล้วสร้าง provider ซึ่งหมายถึงบริษัทหรือบุคคลที่ให้บริการ เลือกให้ดีตั้งแต่แรก เพราะย้าย channel ไป provider อื่นภายหลังไม่ได้ และผู้ใช้ LINE คนเดียวกันจะมี user ID ต่างกันในแต่ละ provider ถ้า Messaging API channel กับ LINE Login channel ต้องใช้ user ID ชุดเดียวกัน ต้องอยู่ใต้ provider เดียวกัน
2. Messaging API channel ตั้งแต่ 4 กันยายน 2024 สร้าง Messaging API channel ใน Console โดยตรงไม่ได้แล้ว ให้สร้าง LINE Official Account ก่อน จากนั้นใน LINE Official Account Manager ไปที่ ตั้งค่า → Messaging API เปิดใช้งานแล้วเลือก provider แล้ว channel จะปรากฏใน Console
3. LINE Login channel สร้างใน Console ใต้ provider เดียวกัน เลือกประเภทแอป (Web app หรือ Mobile app) แล้วใส่ Callback URL ในแท็บ LINE Login
4. LIFF app LIFF app ต้องเพิ่มใน LINE Login channel หรือ LINE MINI App channel (สูงสุด 30 แอปต่อ channel) ไม่ใช่ Messaging API channel แล้วคุณจะได้ LIFF ID และ LIFF URL (https://liff.line.me/{liffId})
Provider "My Shop"
├── Messaging API channel (ผูกกับ LINE Official Account)
├── LINE Login channel (ล็อกอินเว็บ + LIFF apps)
└── LINE MINI App channel (ถ้าต้องการ)
| Credential | ดูได้ที่ | ใช้ทำอะไร |
|---|---|---|
| Channel ID | แท็บ Basic settings | client_id ของ LINE Login ตรวจ ID token ออก token |
| Channel secret | แท็บ Basic settings | ตรวจ signature ของ webhook แลก token ใน LINE Login |
| Channel access token | แท็บ Messaging API (แบบ long-lived) หรือออกผ่าน API | header Authorization: Bearer ตอนเรียก Messaging API |
| LIFF ID | แท็บ LIFF ของ LINE Login หรือ MINI App channel | liff.init({ liffId }) |
Channel access token มี 4 แบบ คือ token ที่กำหนดวันหมดอายุเองได้ (v2.1 อายุไม่เกิน 30 วัน และเป็นแบบที่ LINE แนะนำ) stateless token (15 นาที) short-lived token (30 วัน) และ long-lived token ได้ 1 ตัวต่อ channel (ไม่หมดอายุ ใช้ได้เฉพาะ Messaging API) เก็บ secret ทุกตัวไว้ฝั่งเซิร์ฟเวอร์ใน environment variable หรือ secret manager
# .env
LINE_CHANNEL_ACCESS_TOKEN=...
LINE_CHANNEL_SECRET=...
LINE_LOGIN_CHANNEL_ID=...
LINE_LOGIN_CHANNEL_SECRET=...
#Messaging API: Reply, Push และ Multicast ด้วย fetch
การเรียก Messaging API ทุกครั้งคือ HTTPS request ไปที่ https://api.line.me โดยแนบ channel access token ใน header Authorization ฟังก์ชันช่วยเล็ก ๆ ด้านล่างครอบคลุมงานส่วนใหญ่ของบอท
// line-api.ts: client ขนาดเล็กสำหรับ Messaging API ด้วย fetch (Node.js 22 ขึ้นไป)
const API = "https://api.line.me/v2/bot"
const token = process.env.LINE_CHANNEL_ACCESS_TOKEN
if (!token) throw new Error("Set LINE_CHANNEL_ACCESS_TOKEN")
export type LineMessage = { type: string; [key: string]: unknown }
export class LineApiError extends Error {
constructor(public status: number, public requestId: string | null, public body: string) {
super(`LINE API ${status}: ${body}`)
}
}
async function post(path: string, body: unknown, headers: Record<string, string> = {}) {
const res = await fetch(`${API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, ...headers },
body: JSON.stringify(body),
})
if (!res.ok) throw new LineApiError(res.status, res.headers.get("x-line-request-id"), await res.text())
return res.json() as Promise<{ sentMessages?: { id: string; quoteToken?: string }[] }>
}
// Reply: ใช้ replyToken จาก webhook event ส่งได้สูงสุด 5 ข้อความ และไม่นับโควตา
export function reply(replyToken: string, messages: LineMessage[]) {
return post("/message/reply", { replyToken, messages })
}
// Push: ส่งถึงผู้ใช้ กลุ่ม หรือห้องแชท 1 แห่ง เมื่อส่งซ้ำให้ใช้ retryKey (UUID) เดิม
export function push(to: string, messages: LineMessage[], retryKey: string) {
return post("/message/push", { to, messages }, { "X-Line-Retry-Key": retryKey })
}
// Multicast: สูงสุด 500 user ID ต่อ request
export function multicast(to: string[], messages: LineMessage[]) {
if (to.length > 500) throw new Error("multicast accepts at most 500 user IDs")
return post("/message/multicast", { to, messages })
}
Reply หรือ push การ reply ใช้ replyToken จาก webhook event โดย token ใช้ได้ครั้งเดียว และ LINE ให้ใช้ภายในประมาณ 1 นาทีหลังได้รับ webhook (ช้ากว่านั้นไม่รับประกันว่าจะส่งได้) ข้อความ reply ไม่นับโควตารายเดือน ส่วน push, multicast, broadcast และ narrowcast นับโควตา โดย LINE นับ 1 ข้อความต่อผู้รับ 1 คน ไม่ว่าใน request จะมี message object กี่ชิ้น (สูงสุด 5 ชิ้น) และไม่นับผู้ใช้ที่บล็อกบัญชีไปแล้ว
| วิธีส่ง | Endpoint | ผู้รับ | นับโควตา |
|---|---|---|---|
| Reply | POST /v2/bot/message/reply | ผู้ใช้ กลุ่ม หรือห้องแชทของ event นั้น | ไม่นับ |
| Push | POST /v2/bot/message/push | ผู้ใช้ กลุ่ม หรือห้องแชท 1 แห่ง | นับ |
| Multicast | POST /v2/bot/message/multicast | สูงสุด 500 user ID | นับ |
| Broadcast | POST /v2/bot/message/broadcast | เพื่อนทั้งหมด | นับ |
| Narrowcast | POST /v2/bot/message/narrowcast | audience หรือกลุ่มตามข้อมูลประชากร | นับ |
Push ส่งถึงเพื่อน กลุ่มและห้องแชทที่บัญชีอยู่ และผู้ใช้ที่ทักบัญชีมาภายใน 7 วันแม้ยังไม่ได้เป็นเพื่อน ถ้า push ล้มเหลวเพราะเครือข่ายหรือได้ 5xx ให้ลองใหม่ด้วย X-Line-Retry-Key ค่าเดิม ถ้าครั้งแรก LINE รับไปแล้ว จะได้ 409 กลับมาแทนการส่งข้อความซ้ำ
import { randomUUID } from "node:crypto"
import { push, type LineMessage } from "./line-api.js"
const userId = "U0123456789abcdef0123456789abcdef"
const welcome: LineMessage[] = [
{
type: "text",
text: "คำสั่งซื้อ #1042 จัดส่งแล้วค่ะ",
quickReply: {
items: [
{ type: "action", action: { type: "uri", label: "ติดตามพัสดุ", uri: "https://example.com/track/1042" } },
{ type: "action", action: { type: "message", label: "คุยกับเรา", text: "สอบถามเรื่องคำสั่งซื้อ #1042" } },
],
},
},
]
const retryKey = randomUUID()
const result = await push(userId, welcome, retryKey)
console.log(result.sentMessages?.map((m) => m.id))
ข้อความแต่ละแบบเป็น JSON object 3 แบบที่ใช้บ่อยที่สุดคือ
{ "type": "text", "text": "สวัสดีค่ะ มีอะไรให้ช่วยไหมคะ" }
{
"type": "image",
"originalContentUrl": "https://example.com/photo.jpg",
"previewImageUrl": "https://example.com/photo-preview.jpg"
}
{
"type": "flex",
"altText": "โปรสุดสัปดาห์: ลด 20%",
"contents": {
"type": "bubble",
"hero": {
"type": "image",
"url": "https://example.com/promo.jpg",
"size": "full",
"aspectRatio": "20:13",
"aspectMode": "cover"
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{ "type": "text", "text": "โปรสุดสัปดาห์", "weight": "bold", "size": "xl" },
{ "type": "text", "text": "เครื่องดื่มทุกเมนูลด 20% ถึงวันอาทิตย์", "wrap": true }
]
},
"footer": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "button",
"style": "primary",
"action": { "type": "uri", "label": "ดูเมนู", "uri": "https://example.com/menu" }
}
]
}
}
}
ข้อความ text ยาวได้สูงสุด 5,000 ตัวอักษร quick reply มีได้สูงสุด 13 ปุ่ม และรูปภาพหรือลิงก์ทุกตัวต้องเป็น HTTPS จำนวนข้อความฟรีต่อเดือนขึ้นกับแพ็กเกจ LINE OA และประเทศ ดูรายละเอียดในคู่มือราคา LINE Official Account และเช็กยอดใช้งานได้ด้วย GET /v2/bot/message/quota กับ GET /v2/bot/message/quota/consumption
#LINE Login: Authorization Code Flow แบบ PKCE
LINE Login ทำงานตามมาตรฐาน OAuth 2.0 และ OpenID Connect ขั้นตอนมีดังนี้
- เซิร์ฟเวอร์ redirect ผู้ใช้ไปที่
https://access.line.me/oauth2/v2.1/authorizeพร้อมstate(กัน cross-site request forgery)nonce(กัน replay) และcode_challengeของ PKCE - ผู้ใช้ล็อกอินและกดอนุญาตบนหน้า consent ของ LINE
- LINE redirect กลับมาที่ Callback URL พร้อม
codeและstateหรือerror=ACCESS_DENIEDถ้าผู้ใช้ไม่อนุญาต - เซิร์ฟเวอร์แลก code ที่
POST https://api.line.me/oauth2/v2.1/tokenพร้อมส่งcode_verifier - เซิร์ฟเวอร์ให้ LINE ตรวจ ID token ที่
POST https://api.line.me/oauth2/v2.1/verifyค่าsubคือ user ID
// login.ts: ปุ่ม "เข้าสู่ระบบด้วย LINE" (authorization code flow + PKCE) บน Express 5
import express from "express"
import { createHash, randomBytes } from "node:crypto"
const CHANNEL_ID = process.env.LINE_LOGIN_CHANNEL_ID!
const CHANNEL_SECRET = process.env.LINE_LOGIN_CHANNEL_SECRET!
const REDIRECT_URI = "https://example.com/auth/line/callback" // ต้องตรงกับ Callback URL ที่ตั้งไว้ใน Console
const app = express()
// ที่เก็บชั่วคราวสำหรับเดโม ใน production ให้ใช้ session store หรือฐานข้อมูล
const pending = new Map<string, { nonce: string; codeVerifier: string }>()
function readCookie(header: string | undefined, name: string) {
return header?.split(/;\s*/).find((c) => c.startsWith(`${name}=`))?.slice(name.length + 1)
}
app.get("/auth/line", (_req, res) => {
const state = randomBytes(16).toString("hex")
const nonce = randomBytes(16).toString("hex")
const codeVerifier = randomBytes(32).toString("base64url") // ยาว 43 ตัวอักษร
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url")
pending.set(state, { nonce, codeVerifier })
res.cookie("line_state", state, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 10 * 60 * 1000 })
const params = new URLSearchParams({
response_type: "code",
client_id: CHANNEL_ID,
redirect_uri: REDIRECT_URI,
state,
scope: "profile openid",
nonce,
code_challenge: codeChallenge,
code_challenge_method: "S256",
})
// URLSearchParams แปลงช่องว่างเป็น "+" แต่ตัวอย่างของ LINE ใช้ %20
res.redirect(`https://access.line.me/oauth2/v2.1/authorize?${params.toString().replaceAll("+", "%20")}`)
})
app.get("/auth/line/callback", async (req, res) => {
const { code, state, error } = req.query
const saved = typeof state === "string" ? pending.get(state) : undefined
if (typeof state === "string") pending.delete(state)
if (error || typeof code !== "string" || !saved || state !== readCookie(req.headers.cookie, "line_state")) {
res.status(400).send("Login cancelled or invalid state") // ได้ error=ACCESS_DENIED เมื่อผู้ใช้กดไม่อนุญาต
return
}
// 1. แลก code เป็น token
const tokenRes = await fetch("https://api.line.me/oauth2/v2.1/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CHANNEL_ID,
client_secret: CHANNEL_SECRET,
code_verifier: saved.codeVerifier,
}),
})
if (!tokenRes.ok) {
res.status(502).send("Token exchange failed")
return
}
const { access_token, id_token } = (await tokenRes.json()) as { access_token: string; id_token: string }
// 2. ให้ LINE ตรวจ ID token (ลายเซ็น, channel ID, วันหมดอายุ, nonce)
const verifyRes = await fetch("https://api.line.me/oauth2/v2.1/verify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ id_token, client_id: CHANNEL_ID, nonce: saved.nonce }),
})
if (!verifyRes.ok) {
res.status(401).send("Invalid ID token")
return
}
const claims = (await verifyRes.json()) as { sub: string; name?: string; picture?: string }
// 3. (ไม่บังคับ) ดึงโปรไฟล์ล่าสุดด้วย access token
const profile = await fetch("https://api.line.me/v2/profile", {
headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json() as Promise<{ userId: string; displayName: string; pictureUrl?: string }>)
// claims.sub คือ LINE user ID ของผู้ใช้ภายใต้ provider ของคุณ สร้าง session ตรงนี้
console.log(claims.sub, profile.displayName)
res.redirect("/")
})
app.listen(3000)
ข้อควรรู้
stateต้องเป็นค่าสุ่มที่มีแต่ตัวอักษรและตัวเลข สร้างใหม่ทุกครั้งที่ล็อกอินและผูกกับเบราว์เซอร์ (ตัวอย่างนี้ใช้ cookie แบบ HttpOnly) ถ้าค่าไม่ตรงให้ปฏิเสธ callback ทันที- scope
emailใช้ได้หลังจาก LINE อนุมัติสิทธิ์อีเมลให้ channel ของคุณแล้วเท่านั้น - ใส่
bot_prompt=normalหรือbot_prompt=aggressiveใน URL เพื่อชวนผู้ใช้เพิ่ม LINE OA เป็นเพื่อนระหว่างล็อกอิน แล้ว callback จะมีค่าfriendship_status_changedกลับมาด้วย
#LIFF: ส่ง ID Token ไปตรวจที่เซิร์ฟเวอร์
LIFF app คือเว็บแอปทั่วไปบน endpoint ที่เป็น HTTPS ซึ่ง LINE เปิดใน LIFF browser ติดตั้ง @line/liff จาก npm เวอร์ชัน 2.31.1 ขึ้นไป (เมื่อ 30 กันยายน 2026 LINE ประกาศเลิกรองรับ v2.20.0 ถึง v2.31.0 เพราะปัญหาด้านความปลอดภัย) หรือโหลดจาก https://static.line-scdn.net/liff/edge/2/sdk.js ซึ่งให้เวอร์ชัน 2.x ล่าสุดเสมอ
// LIFF app (ฝั่งเบราว์เซอร์): ขอ ID token แล้วส่งให้เซิร์ฟเวอร์ตรวจ
import liff from "@line/liff"
await liff.init({ liffId: "1234567890-AbcdEfgh" })
if (!liff.isLoggedIn()) {
liff.login() // ใช้ในเบราว์เซอร์ภายนอก ส่วนใน LIFF browser ผู้ใช้ล็อกอินอยู่แล้ว
} else {
const idToken = liff.getIDToken() // ต้องเปิด scope openid
await fetch("/api/me", { method: "POST", headers: { Authorization: `Bearer ${idToken}` } })
}
อย่าส่งผลลัพธ์จาก liff.getProfile() ไปให้เซิร์ฟเวอร์เป็นหลักฐานยืนยันตัวตน เพราะ request body ปลอมได้ง่าย ให้ส่ง ID token (หรือ access token) แล้วตรวจที่เซิร์ฟเวอร์ด้วย POST /oauth2/v2.1/verify แบบเดียวกับหัวข้อ LINE Login โดยใช้ channel ID ของ LINE Login channel ที่ LIFF app อยู่ เรื่องขนาดหน้าจอ liff.sendMessages() share target picker การเปิดในเบราว์เซอร์ภายนอก และการ deploy อ่านต่อได้ในคู่มือพัฒนา LIFF app
#Webhook: ตรวจ Signature และจัดการ Event
เมื่อมีเหตุการณ์ในแชท (ข้อความใหม่ เพื่อนใหม่ การแตะปุ่ม postback) LINE จะส่ง HTTPS POST ไปที่ webhook URL ซึ่งมีได้ URL เดียวต่อ Messaging API channel ตั้งค่าได้ในแท็บ Messaging API ของ Console กด Verify แล้วเปิด Use webhook URL ต้องใช้ใบรับรองจากผู้ออกใบรับรองที่เบราว์เซอร์ทั่วไปเชื่อถือ (ใช้ self-signed ไม่ได้) ถ้าเซิร์ฟเวอร์ของคุณเป็นผู้ตอบลูกค้า ให้ปิดข้อความทักทายและข้อความตอบกลับอัตโนมัติใน LINE Official Account Manager เพื่อไม่ให้ลูกค้าได้คำตอบซ้ำ 2 ข้อความ
LINE เซ็นทุก request โดย header x-line-signature คือค่า HMAC-SHA256 ของ request body ดิบ ที่ใช้ channel secret เป็น key แล้วเข้ารหัส Base64 ต้องตรวจกับ byte ที่ได้รับมาจริงก่อนแปลง JSON ถ้าแปลง body แล้ว serialize กลับ (อย่างที่ JSON.stringify(req.body) ทำหลัง express.json()) byte จะเปลี่ยนและตรวจไม่ผ่าน
// webhook.ts: รับ event โดยไม่ใช้ SDK (Express 5)
import express from "express"
import { createHmac, timingSafeEqual } from "node:crypto"
import { reply } from "./line-api.js"
const CHANNEL_SECRET = process.env.LINE_CHANNEL_SECRET!
const app = express()
function isValidSignature(rawBody: Buffer, signature: string | undefined) {
if (!signature) return false
const expected = createHmac("sha256", CHANNEL_SECRET).update(rawBody).digest()
const received = Buffer.from(signature, "base64")
return received.length === expected.length && timingSafeEqual(received, expected)
}
type LineEvent = {
type: string
webhookEventId: string
deliveryContext: { isRedelivery: boolean }
replyToken?: string
source?: { type: string; userId?: string; groupId?: string; roomId?: string }
message?: { id: string; type: string; text?: string }
postback?: { data: string }
}
// express.raw เก็บ byte ตามที่ LINE เซ็นไว้ทุกตัว ห้ามใช้ express.json() กับ route นี้
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body
if (!Buffer.isBuffer(rawBody) || !isValidSignature(rawBody, req.get("x-line-signature"))) {
res.sendStatus(401)
return
}
const { events } = JSON.parse(rawBody.toString("utf8")) as { events: LineEvent[] }
res.sendStatus(200) // ตอบให้เร็ว รวมถึงตอนที่ events เป็น [] (LINE ทดสอบการเชื่อมต่อ)
for (const event of events) handleEvent(event).catch((err) => console.error(err))
})
const seen = new Set<string>() // ใน production ให้ใช้ Redis หรือฐานข้อมูลที่มี TTL
async function handleEvent(event: LineEvent) {
if (seen.has(event.webhookEventId)) return // event ที่ส่งซ้ำหรือซ้ำกัน
seen.add(event.webhookEventId)
switch (event.type) {
case "message":
if (event.replyToken && event.message?.type === "text") {
await reply(event.replyToken, [{ type: "text", text: `คุณพิมพ์ว่า: ${event.message.text}` }])
}
break
case "follow":
if (event.replyToken) await reply(event.replyToken, [{ type: "text", text: "ขอบคุณที่เพิ่มเราเป็นเพื่อนค่ะ" }])
break
case "postback": {
const data = new URLSearchParams(event.postback?.data) // เช่น "action=book&slot=10:00"
console.log("postback", data.get("action"), data.get("slot"))
break
}
case "unfollow":
// ไม่มี reply token: ผู้ใช้บล็อกบัญชีแล้ว หยุดส่งถึง event.source?.userId
break
}
}
app.listen(Number(process.env.PORT ?? 3000))
สิ่งที่โค้ดนี้ทำตามเอกสารของ LINE
- ตอบ
200กับทุก request ที่ถูกต้อง รวมถึง request ที่มี"events": []ซึ่ง LINE ส่งมาเพื่อทดสอบการเชื่อมต่อ - ประมวลผล event แบบ asynchronous เพื่อไม่ให้ handler ที่ช้าไปถ่วง webhook ถัดไป ถ้าใช้แพลตฟอร์ม serverless ที่หยุดทำงานทันทีหลังส่ง response ให้ส่ง event เข้าคิวก่อนตอบ
- LINE ไม่เปิดเผย IP address ที่ใช้ส่ง webhook ดังนั้น signature คือการยืนยันตัวตนของ request
- ฟีเจอร์ส่ง webhook ซ้ำ (redelivery) ปิดอยู่โดยค่าเริ่มต้น ถ้าเปิดใน Console แล้ว LINE จะส่ง webhook ที่ไม่ได้รับ 2xx ซ้ำ และ event เดียวกันอาจมาถึง 2 ครั้ง ให้ข้าม event ที่
webhookEventIdเคยประมวลผลแล้ว โดยdeliveryContext.isRedeliveryบอกว่าเป็น event ที่ส่งซ้ำ
| Event | เกิดขึ้นเมื่อ | Reply token |
|---|---|---|
message | ผู้ใช้ส่งข้อความ รูป วิดีโอ เสียง ไฟล์ ตำแหน่ง หรือสติกเกอร์ | มี |
follow | ผู้ใช้เพิ่มบัญชีเป็นเพื่อนหรือเลิกบล็อก | มี |
unfollow | ผู้ใช้บล็อกบัญชี | ไม่มี |
postback | ผู้ใช้แตะ action แบบ postback (ปุ่ม Rich Menu quick reply) | มี |
unsend | ผู้ใช้ยกเลิกการส่งข้อความ | ไม่มี |
join / leave | บัญชีเข้ากลุ่มหรือห้องแชท / ออกหรือถูกเชิญออก | join: มี, leave: ไม่มี |
memberJoined / memberLeft | มีคนเข้า / ออกจากกลุ่มที่บัญชีอยู่ | joined: มี, left: ไม่มี |
messageEdited | ผู้ใช้แก้ไขข้อความ text ในแชทกลุ่ม (เพิ่มเมื่อ 12 สิงหาคม 2026) | มี |
videoPlayComplete, beacon, accountLink, membership | ดูวิดีโอจนจบ, LINE Beacon, การผูกบัญชี, ระบบสมาชิก | มี |
Webhook เดียวกันในภาษา Python ด้วย SDK ทางการ (line-bot-sdk v3, Python 3.10 ขึ้นไป) โดย WebhookHandler.handle() จะตรวจ signature และโยน InvalidSignatureError เมื่อไม่ผ่าน
# app.py: webhook เดียวกันในภาษา Python ด้วย line-bot-sdk v3 (Python 3.10 ขึ้นไป)
import os
from flask import Flask, request, abort
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 FollowEvent, 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) # body ดิบ ก่อนแปลงเป็น JSON
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(401)
return "OK"
@handler.add(MessageEvent, message=TextMessageContent)
def on_text(event):
with ApiClient(configuration) as api_client:
MessagingApi(api_client).reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text=f"คุณพิมพ์ว่า: {event.message.text}")],
)
)
@handler.add(FollowEvent)
def on_follow(event):
with ApiClient(configuration) as api_client:
MessagingApi(api_client).reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text="ขอบคุณที่เพิ่มเราเป็นเพื่อนค่ะ")],
)
)
#ความปลอดภัย, Rate Limit และการจัดการ Error
เช็กลิสต์ความปลอดภัย
- ตรวจ signature ของ webhook ทุกครั้งจาก body ดิบ และเทียบค่าแบบ constant-time
- เก็บ channel secret และ token ไว้ฝั่งเซิร์ฟเวอร์ใน environment variable หรือ secret manager และเพิกถอน token ที่สงสัยว่ารั่ว
- ใช้ channel access token v2.1 หรือ short-lived token เป็นหลัก ถ้ายังใช้ long-lived token ให้จำกัด IP ที่เรียก API ได้ในแท็บ Security ของ Console
- ตรวจ ID token ของ LINE Login และ LIFF ที่เซิร์ฟเวอร์ ห้ามเชื่อ user ID ที่เบราว์เซอร์ส่งมา
- ใช้ HTTPS ทุกจุด และตรวจสอบหรือ escape ข้อมูลจากผู้ใช้ก่อนบันทึกหรือแสดงผล
- เรียก endpoint ให้ตรงตามเอกสารทุกตัวอักษร เมื่อ 17 สิงหาคม 2026 LINE เตือนว่า path ที่ต่างไป เช่นมีเครื่องหมาย / ต่อท้าย อาจทำงานผิดปกติ
Rate limit (ต่อ channel ณ เดือนตุลาคม 2026)
| Endpoint | ขีดจำกัด |
|---|---|
| Reply, push และ endpoint ส่วนใหญ่ | 2,000 request ต่อวินาที |
| Multicast (ตั้งแต่ 23 เมษายน 2025) | 200 request ต่อวินาที |
| Get rich menu list (ตั้งแต่ 26 พฤษภาคม 2026) | 10 request ต่อวินาที |
| Display a loading animation | 100 request ต่อวินาที |
| Broadcast, narrowcast, สถิติ, test webhook endpoint | 60 request ต่อชั่วโมง |
| สร้างหรือลบ Rich Menu ผ่าน API | 100 request ต่อชั่วโมง |
Error จะส่งกลับเป็น JSON ที่มี message และถ้า request ผิดรูปแบบจะมี array details ด้วย
| Status | ความหมาย | ควรทำอย่างไร |
|---|---|---|
| 400 | Request ไม่ถูกต้อง หรือ Invalid reply token (token ถูกใช้แล้วหรือหมดอายุ) | แก้ payload ตาม details อย่าส่งซ้ำแบบเดิม |
| 401 | Channel access token ไม่ถูกต้อง | ออก token ใหม่ |
| 403 | บัญชีหรือแพ็กเกจใช้ endpoint นี้ไม่ได้ | ตรวจแพ็กเกจและสิทธิ์ |
| 409 | X-Line-Retry-Key เดียวกันถูกรับไปแล้ว | ถือว่าส่งสำเร็จ |
| 413 | Request ใหญ่เกิน 2 MB | ลดขนาดข้อมูลต่อ request |
| 429 | เกิน rate limit หรือส่งครบโควตารายเดือนแล้ว ("You have reached your monthly limit.") | รอแล้วค่อยลองใหม่ และตรวจโควตากับแพ็กเกจ |
| 500 | ข้อผิดพลาดฝั่ง LINE | ลองใหม่แบบเว้นระยะ สำหรับ push ใช้ retry key เดิม |
import { randomUUID } from "node:crypto"
import { LineApiError, push, type LineMessage } from "./line-api.js"
export async function pushWithRetry(to: string, messages: LineMessage[], attempts = 3) {
const retryKey = randomUUID() // ใช้ key เดิมทุกครั้งที่ลองใหม่
for (let attempt = 1; ; attempt++) {
try {
return await push(to, messages, retryKey)
} catch (err) {
if (err instanceof LineApiError && err.status === 409) return // LINE รับไปแล้วตั้งแต่ครั้งก่อน
const retryable = !(err instanceof LineApiError) || err.status === 429 || err.status >= 500
if (!retryable || attempt >= attempts) throw err
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000))
}
}
}
บันทึก header x-line-request-id ของ response ไว้ใน log ด้วย (ตัวอย่างด้านบนเก็บไว้ใน LineApiError.requestId) เพราะทีมซัพพอร์ตของ LINE จะขอค่านี้เมื่อแจ้งปัญหา
#LineBot.pro ช่วยตรงไหนได้บ้าง
LineBot.pro ไม่ใช่ SDK หรือผู้ให้บริการ API และไม่มี public API ทุกอย่างในบทความนี้ทำงานบนโค้ดและ channel ของคุณเอง แต่มี 2 ส่วนที่ช่วยประหยัดเวลาได้
- ออกแบบข้อความและเมนู ตัวสร้างด้วย AI ทำ Rich Message (Flex bubble ที่มีรูปหลัก หัวข้อ ข้อความ และปุ่ม) หรือรูป Rich Menu ที่มีพื้นที่กด 1 ถึง 6 ช่องได้ คัดลอก Flex JSON ไปใส่ในฟังก์ชัน
replyหรือpushด้านบนได้เลย หรือดาวน์โหลดรูปและ JSON ของเมนูไปอัปโหลดเอง Rich Message ใช้ 8 เครดิต Rich Menu ใช้ 10 เครดิต บัญชีใหม่ได้ฟรี 50 เครดิต และเครดิตซื้อเป็นแพ็กแบบจ่ายครั้งเดียวที่ไม่มีวันหมดอายุ (ดูราคา) สร้าง Flex Message - บอทแบบไม่ต้องเขียนโค้ด ถ้าแค่ตอบตาม keyword ก็พอ เครื่องมือแชทบอท LINE ของเราตอบ keyword ที่ตรงด้วยการ์ด Rich Message ที่คุณอนุมัติไว้ในแชท 1:1 และเลือกเปิด AI ช่วยจับเจตนาของข้อความที่ไม่ตรง keyword ได้ (1 เครดิตต่อข้อความที่ตรวจ AI แค่เลือกการ์ด ไม่ได้เขียนคำตอบเอง) เครื่องมือนี้ใช้ webhook URL ของตัวเอง และ 1 channel มี webhook URL ได้ URL เดียว จึงใช้แทนเซิร์ฟเวอร์ webhook ของคุณบน channel นั้น ไม่ได้ทำงานคู่กัน
ถ้าต้องการเชื่อม Messaging API กับระบบของคุณ ทำ LIFF app หรือ LINE MINI App ทีมเรารับพัฒนาระบบ LINE ตามความต้องการ และพัฒนาแอปบน LINE ติดต่อเรา พร้อมเล่าโจทย์ของคุณได้เลย
#คำถามที่พบบ่อย
Q: LINE API ทำอะไรได้บ้าง? ด้วย Messaging API บัญชี LINE OA สามารถตอบกลับผู้ใช้ ส่ง push, multicast, broadcast และ narrowcast จัดการ Rich Menu และดึงโปรไฟล์กับสถิติได้ LINE Login เพิ่มปุ่ม "เข้าสู่ระบบด้วย LINE" ให้เว็บและแอป LIFF เปิดเว็บแอปใน LINE และ LINE MINI App นำเว็บแอปเหล่านั้นไปเผยแพร่เป็นบริการใน LINE
Q: LINE Messaging API ใช้ฟรีไหม? การเรียก API ไม่มีค่าใช้จ่าย ข้อความ reply ไม่นับโควตาเลย ส่วน push, multicast, broadcast และ narrowcast นับเข้าโควตาข้อความรายเดือนของแพ็กเกจ 1 ข้อความต่อผู้รับ 1 คน ณ เดือนตุลาคม 2026 แพ็กเกจฟรีส่งได้เดือนละ 300 ข้อความในไทย 200 ข้อความในญี่ปุ่น และ 200 ข้อความในไต้หวัน แพ็กเกจแบบเสียเงินดูได้ในคู่มือราคา
Q: ทำไมตรวจ signature ของ webhook ไม่ผ่าน?
ส่วนใหญ่เป็นเพราะ body ถูกแปลงเป็น JSON ก่อนตรวจ (เช่นโดย express.json()) ทำให้ hash คนละ byte กับที่ LINE เซ็น สาเหตุอื่นคือใช้ channel secret ของ channel อื่นหรือ secret ที่ถูกออกใหม่แล้ว มี proxy แก้ body หรือใช้อัลกอริทึมผิด ให้ตรวจ body ดิบด้วย HMAC-SHA256 แล้วเข้ารหัส Base64
Q: ยังใช้ LINE Notify ได้อยู่ไหม? ไม่ได้ LINE Notify ปิดให้บริการตั้งแต่ 31 มีนาคม 2025 ให้ส่งการแจ้งเตือนด้วย push message ของ Messaging API จาก LINE OA แทน โดยผู้รับต้องเป็นเพื่อนกับบัญชี
Q: ต้องใช้ SDK ไหมถึงจะเรียก LINE API ได้? ไม่จำเป็น ทุก API เป็น HTTPS กับ JSON ธรรมดาตามตัวอย่างในบทความนี้ SDK ทางการ (Node.js, Python, PHP, Java, Go และ Ruby) ช่วยเรื่อง type ฟังก์ชันตรวจ signature และ path ของ endpoint ที่ถูกต้อง ส่วนภาษาอื่น LINE มี OpenAPI specification ให้ใช้
Q: LIFF ต่างจาก LINE Login อย่างไร? LINE Login ให้ผู้ใช้ล็อกอินเว็บไซต์หรือแอปของคุณนอก LINE ส่วน LIFF เปิดเว็บแอปของคุณภายใน LINE โดยเบื้องหลังใช้ LINE Login จึงต้องเพิ่ม LIFF app ใน LINE Login channel (หรือ LINE MINI App channel)
ขั้นตอนต่อไป:
- สร้างบอทเดียวกันด้วย SDK: ตัวอย่าง @line/bot-sdk Express middleware
- ต่อยอดเรื่อง conversation flow: สอนเขียน LINE Bot ด้วย Node.js และ Python
- ทำหน้าเว็บใน LINE: คู่มือพัฒนา LIFF app
- ตามทันการเปลี่ยนแปลงของแพลตฟอร์ม: อัปเดต LINE Messaging API 2026
- อยากให้เราพัฒนาให้: รับพัฒนาระบบ LINE
บริการที่เกี่ยวข้อง
เกี่ยวกับ LineBot.pro
LineBot.pro คือเครื่องมือ AI สำหรับ LINE Official Account ที่ช่วยสร้าง Rich Menu, Rich Message และแบนเนอร์ ส่งแคมเปญบรอดแคสต์ และตอบกลับอัตโนมัติด้วยคำสำคัญ ทีมงานของเราเขียนคู่มือเหล่านี้จากประสบการณ์จริงกับ LINE Messaging API
ติดต่อเราบทความที่เกี่ยวข้อง

ตัวอย่าง @line/bot-sdk Express Middleware (v11, TypeScript)
โค้ด webhook ด้วย Express + TypeScript สำหรับ @line/bot-sdk v11 ที่ตรวจ type แล้ว: วาง middleware() ตรงไหน, validateSignature ทำงานอย่างไรใน Node, Python และ PHP, replyMessage พร้อม Flex Message, sentMessages กับ quote token และ error ที่เจอจริง
อ่านเพิ่มเติม
สอนพัฒนา LIFF App 2026 ด้วย Next.js และ TypeScript
วิธีสร้าง LIFF (LINE Front-end Framework) app ในปี 2026 ตั้งแต่ตั้งค่า channel และ endpoint URL กฎของ liff.init การยืนยัน ID token ฝั่งเซิร์ฟเวอร์ sendMessages shareTargetPicker deep link การทดสอบด้วย LIFF CLI และอัปเดตความปลอดภัย v2.31.1
อ่านเพิ่มเติม
สอนเขียน LINE Bot ด้วย Node.js และ Python (Messaging API) 2026
สอนเขียน LINE Bot ทีละขั้นด้วย Messaging API ตั้งแต่เปิดใช้ channel ตรวจลายเซ็น webhook ตอบด้วย Flex Message และ quick reply ทำขั้นตอนจองคิวด้วย postback เพิ่มคำตอบจาก AI ที่จำกัดอยู่ใน FAQ ของร้าน ไปจนถึง deploy พร้อมโค้ด Node.js (@line/bot-sdk v11) และ Python (line-bot-sdk v3)
อ่านเพิ่มเติมพร้อมให้ LINE OA ของคุณทำงานอัตโนมัติหรือยัง?
เริ่มสร้างคอนเทนต์และระบบตอบกลับอัตโนมัติบน LINE กับ LineBot.pro วันนี้