เฉลยวิธีส่ง Service Message บน LINE MINI App ภารกิจพิชิตแต้ม LINE HACK 2025 ในเวลาไม่ถึงชั่วโมง
เฉลยวิธีส่ง Service Message บน LINE MINI App แบบง่าย ๆ ภารกิจพิชิตแต้ม LINE HACK 2025 ในเวลาไม่ถึงชั่วโมง

หนึ่งในฟีเจอร์ที่หลาย ๆ คนน่าจะรอคอยมากที่สุด ที่อยากใช้งานกันใน LINE MINI App ก็คงไม่พ้น Service Message บริการส่งข้อความแจ้งเตือนแบบฟรี ๆ ไม่มีค่าใช้จ่าย
เมื่อปลายเดือนพฤศจิกายนในงานการแข่งขัน LINE HACK 2025 ที่ผ่านมา ก็มีการทำภารกิจโดยใช้ Service Message บน LINE MINI App เพื่อรับคะแนนพิเศษเช่นกัน ภายในระยะเวลาไม่เกิน 1 ชั่วโมง
ในบทความนี้จะมาเฉลยข้อสอบ ภารกิจ ‘ส่ง Service Message บน LINE MINI App’ ในระยะเวลาไม่ถึง 1 ชั่วโมง สามารถทำตามได้ง่าย ๆ เลย
โจทย์ (Requirement)
เมื่อเปิด LINE MINI App จะมีการส่ง Service Message ให้กับผู้ใช้งาน เมื่อทำการส่งสำเร็จจะมี Alert “Message sent successfully!” และทำการปิด LINE MINI App ในทันที

ก่อนอื่น เราต้องมาแบ่งกันก่อนว่าจะต้องทำส่วนไหนบ้าง??
วางแผน
การส่ง Service Message ผ่าน LINE MINI App จะแบ่งเป็น 2 ส่วน
- Front-end — หน้าเว็บแอปที่ Integrate ตัว LIFF SDK เพื่อดึง LIFF Access Token และส่งไปยัง Back-end
- Back-end — เซิร์ฟเวอร์รับ LIFF Access Token นำมาส่ง Service Message ให้ผู้ใช้ โดยทำเป็น API
POST /sendและส่ง LIFF Access Token มาใน Header ที่ชื่อว่าx-liff-access-token
เทคโนโลยีที่นำมาใช้ในการสาธิตในครั้งนี้คือ Cloudflare Pages เนื่องจากเราสามารถทำได้แบบง่าย ๆ พร้อมสามารถ Deploy ได้ในทันที
โครงสร้าง
โครงสร้างของโปรเจกต์ทำได้ง่าย ๆ ไม่สับซ้อน มีเพียง 2 ไฟล์ เท่านั้น index.html (อยู่ที่ root) และ send.js (อยู่ภายใต้โฟลเดอร์ functions)
line-mini-app/
├─ .dev.vars
├─ index.html
├─ functions/
│ ├─ send.js
เริ่มกันเลย
เริ่มต้นสร้าง LINE MINI App และส่ง Service Message ให้กับผู้ใช้งาน
ก่อนอื่นจะต้องสร้าง LINE MINI App บน LINE Developers Console หากใครยังไม่มีสามารถสร้างตามขั้นตอนในบทความ “ย้ายบ้านจาก LIFF สู่ LINE MINI App ได้ง่าย ๆ ยิ่งกว่าปอกกล้วย” ได้เลย

เมื่อได้ LINE MINI App มาเรียบร้อยแล้ว ในแท็บ ‘Service message template’ กด ‘Add’ เพื่อเพิ่มเทมเพลตที่ต้องการ ซึ่งในโจทย์ด้านบนคือเทมเพลต couponnoti_s_c_th (Category: Coupon / Language: Thai / Template name: Coupon notification (simple))

หลังจากเลือกเทมเพลตเรียบร้อยแล้ว ใส่ Use case ที่ต้องการใช้งาน (สำหรับการทดสอบใส่เป็นอะไรก็ได้ก่อน) และกด ‘Add’ เพื่อสร้างเทมเพลต

สำหรับการทดสอบในครั้งนี้ เราจะใช้แชนเนลที่เป็น Developing ในการพัฒนาเท่านั้น เพราะในการส่ง Service Message บนแชนเนลที่เป็น Published จะสามารถทำได้บนเฉพาะ Verified MINI App เท่านั้น
เมื่อเตรียมทุกอย่างบน LINE Developers Console เสร็จเรียบร้อย เราจะมาเริ่มโค้ดกันเลย!!
อย่างแรก เราจะทำการสร้าง Functions ก่อน เพื่อเป็น API Endpoint POST /sendในการส่ง Service Message ตามสเปกต่าง ๆ ที่มีบน LINE Developers ในไฟล์ functions/send.js ตามผังไฟล์ด้านบน (สามารถคัดลอกเทมเพลตจากด้านล่างนี้ได้)
async function issueStatelessChannelAccessToken(env) {
return "";
}
async function issueServiceNotificationToken(
channelAccessToken,
liffAccessToken
) {
return "";
}
async function sendServiceMessage(
channelAccessToken,
serviceNotificationToken
) {
return "";
}
export async function onRequest(context) {
// 1. Only allow POST requests
// 2. Get the LIFF access token from the request headers
// 3. Issue Stateless Channel Access Token
const statelessChannelAccessToken = await issueStatelessChannelAccessToken(
context.env
);
// -- Service Message --
// 4. Issuing a service notification token
const serviceNotificationToken = await issueServiceNotificationToken(
statelessChannelAccessToken,
liffAccessToken
);
// 5. Sending a service message
const serviceMessageResponse = await sendServiceMessage(
statelessChannelAccessToken,
serviceNotificationToken
);
return new Response("Message Sent", { status: 200 });
}
โดยมีการทำงานดังนี้
- กรอง Request Methodให้ประมวลผลเฉพาะ
POSTเท่านั้น - ดึง LIFF Access Token ออกมาจาก Header
x-liff-access-token - ออก Stateless Channel Access Token
- ออก Service Notification Token
- ส่ง Service Message
เริ่มต้นด้วยด้านบนสุดหาก Request ที่รับมาเป็น Request ที่ยิงมาด้วย Method อื่น ๆ ที่ไม่ใช่ POST ก็จะถูกปัดออกไปไม่ให้ประมวลผลต่อ
// 1. Only allow POST requests
if (context.request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
ในการส่งในการสาธิตครั้งนี้ กำหนดให้ส่ง LIFF Access Token มาใน Header ที่ชื่อว่า x-liff-access-token หากไม่ได้ส่งมา จะถูกตีเป็น Error Unauthorized
// 2. Get the LIFF access token from the request headers
const liffAccessToken = context.request.headers.get("x-liff-access-token");
if (!liffAccessToken) {
return new Response("Unauthorized", { status: 401 });
}
การออก Channel Access Token เราจำเป็นที่จะต้องใช้ข้อมูล Channel ID และ Channel Secret ที่ได้จากแชนเนล LINE MINI App ที่เราสร้างไปแล้ว นำมาสร้างไฟล์ .dev.vars (ไฟล์ env ของ Cloudflare)
MINI_APP_CHANNEL_ID=
MINI_APP_CHANNEL_SECRET=
async function issueStatelessChannelAccessToken(env) {
const response = await fetch("https://api.line.me/oauth2/v3/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: env.MINI_APP_CHANNEL_ID,
client_secret: env.MINI_APP_CHANNEL_SECRET,
}),
});
if (!response.ok) {
throw new Error("Failed to obtain channel access token");
}
const data = await response.json();
return data.access_token;
}
เราจะต้องใช้ Channel Access Token ที่สร้างมาในขั้นตอนก่อนหน้านี้ โดยจะใช้ได้เฉพาะ Short-lived Channel Access Token หรือ Stateless Channel Access Token เท่านั้น ร่วมกับ LIFF Access Token ที่ได้มาจาก Header ในการออก Service Notification Token
โดยการออก Service Notification Token 1 ครั้ง จะสามารถใช้ Token ที่ได้ส่ง Service Message ได้ถึง 5 ครั้ง ภายในระยะเวลา 1 ปี (หากมีการส่ง Service Message จะต้องเก็บ Service Notification Token ตัวใหม่ที่อยู่ใน Response เพื่อใช้ในการส่งครั้งถัดไป)
async function issueServiceNotificationToken(
channelAccessToken,
liffAccessToken
) {
const response = await fetch(
"https://api.line.me/message/v3/notifier/token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${channelAccessToken}`,
},
body: JSON.stringify({
liffAccessToken: liffAccessToken,
}),
}
);
if (!response.ok) {
throw new Error("Failed to obtain service notification token");
}
const data = await response.json();
return data.notificationToken;
}
หลังจากได้ Service Notification Token มาเรียบร้อยแล้ว เราสามารถนำ Token ที่ได้และ Channel Access Token มาใช้ควบคู่กันเพื่อส่ง Service Message ได้เลย ใน Body ของการส่ง เราจะมีการกำหนด Template Name ลงไป ซึ่งเป็น Template ที่เราเลือกไว้ก่อนหน้านี้เท่านั้น
async function sendServiceMessage(
channelAccessToken,
serviceNotificationToken
) {
const response = await fetch(
"https://api.line.me/message/v3/notifier/send?target=service",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${channelAccessToken}`,
},
body: JSON.stringify({
templateName: "couponnoti_s_c_th",
params: {
btn1_url: "https://linedevth.line.me"
},
notificationToken: serviceNotificationToken,
}),
}
);
if (!response.ok) {
throw new Error("Failed to send service message");
}
const data = await response.json();
return data;
}
โดยหลังจากการส่ง Service Message สำหรับ เราสามารถนำ Service Notification Token ตัวใหม่ที่ได้จาก Response ของการส่ง นำไปเก็บใน Database เพื่อใช้ในครั้งถัดไปได้
หลังจากสร้าง API ตามขั้นตอนด้านบนเรียบร้อยแล้ว จะได้โค้ดแบบด้านล่างนี้
async function issueStatelessChannelAccessToken(env) {
const response = await fetch("https://api.line.me/oauth2/v3/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: env.MINI_APP_CHANNEL_ID,
client_secret: env.MINI_APP_CHANNEL_SECRET,
}),
});
if (!response.ok) {
throw new Error("Failed to obtain channel access token");
}
const data = await response.json();
return data.access_token;
}
async function issueServiceNotificationToken(
channelAccessToken,
liffAccessToken
) {
const response = await fetch(
"https://api.line.me/message/v3/notifier/token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${channelAccessToken}`,
},
body: JSON.stringify({
liffAccessToken: liffAccessToken,
}),
}
);
if (!response.ok) {
throw new Error("Failed to obtain service notification token");
}
const data = await response.json();
return data.notificationToken;
}
async function sendServiceMessage(
channelAccessToken,
serviceNotificationToken
) {
const response = await fetch(
"https://api.line.me/message/v3/notifier/send?target=service",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${channelAccessToken}`,
},
body: JSON.stringify({
templateName: "couponnoti_s_c_th",
params: {
btn1_url: "https://linedevth.line.me"
},
notificationToken: serviceNotificationToken,
}),
}
);
if (!response.ok) {
throw new Error("Failed to send service message");
}
const data = await response.json();
return data;
}
export async function onRequest(context) {
// Only allow POST requests
if (context.request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
// Get the LIFF access token from the request headers
const liffAccessToken = context.request.headers.get("x-liff-access-token");
if (!liffAccessToken) {
return new Response("Unauthorized", { status: 401 });
}
// Issue Stateless Channel Access Token
const statelessChannelAccessToken = await issueStatelessChannelAccessToken(
context.env
);
// -- Service Message --
// Issuing a service notification token
const serviceNotificationToken = await issueServiceNotificationToken(
statelessChannelAccessToken,
liffAccessToken
);
// Sending a service message
const serviceMessageResponse = await sendServiceMessage(
statelessChannelAccessToken,
serviceNotificationToken
);
return new Response("Message Sent", { status: 200 });
}
ต่อไปหลังจากสร้าง API เสร็จเรียบร้อยแล้ว มาสร้าง LINE MINI App กันต่อ โดยให้สร้างไฟล์ index.html ขึ้นมาคัดลอกจากเทมเพลตด้านล่างไปใส่เพื่อทำไปพร้อมกัน
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover"
/>
<title>LINE MINI App - Simple Service Message</title>
<!-- 1. Import LIFF SDK -->
</head>
<body>
<script>
async function main() {
// 2. Initialize LIFF app with your LIFF ID
// 3. Check if the user is logged in
// 4. Get the LIFF access token
// 5. Send a simple service message
// 6. Check the response status
// 7. Close the LIFF window
}
main();
</script>
</body>
</html>
ก่อนที่จะทำใด ๆ ให้เรา Import LIFF SDK เข้ามาในไฟล์ของเราก่อน
<!-- 1. Import LIFF SDK -->
<script
charset="utf-8"
src="https://static.line-scdn.net/liff/edge/2/sdk.js"
></script>
จากนั้นให้ Init LIFF App ด้วย LIFF ID ของ LINE MINI App ที่สร้างไปก่อนหน้านี้ที่เป็นแชนเนล Developing สามารถดู LIFF ID ได้จาก ID ด้านหลังของ LIFF URL https://miniapp.line.me/{{LIFF_ID}} ในแท็บ Web app settings
// 2. Initialize LIFF app with your LIFF ID
await liff.init({ liffId: "{{LIFF_ID}}" });
ต่อมาเช็กว่าผู้ใช้ได้ล็อกอินแล้วหรือไม่ หากยังไม่ได้ล็อกอิน ให้กลับไปล็อกอินให้เรียบร้อยก่อน เพื่อให้ได้ LIFF Access Token
// 3. Check if the user is logged in
if (!liff.isLoggedIn()) {
// If not logged in, redirect to LINE login
liff.login();
return;
}
เมื่อผู้ใช้ทำการล็อกอินเรียบร้อยแล้ว ให้ถอด LIFF Access Token ออกมาเพื่อส่งในขั้นตอนถัดไป
// 4. Get the LIFF access token
const liffAccessToken = liff.getAccessToken();
ในขั้นตอนนี้หลังจากที่ได้ LIFF Access Token มาเรียบร้อยแล้ว ให้ส่งไปใน API ที่เราสร้างกันไปก่อนหน้านี้ /send ด้วย Method POST พร้อมกับ LIFF Access Token ใน Header x-liff-access-token
// 5. Send a simple service message
const response = await fetch("/send", {
method: "POST",
headers: {
"x-liff-access-token": liffAccessToken,
},
});
จากนั้นรอให้เซิร์ฟเวอร์ทำการส่ง Service Message และดู Response ที่ตอบกลับมาว่าส่งสำเร็จหรือไม่ และขึ้น Alert เพื่อแจ้งเตือนผู้ใช้งาน
// 6. Check the response status
if (response.ok) {
alert("Message sent successfully!");
} else {
alert("Failed to send message.");
}
เมื่อแจ้งเตือนผู้ใช้แล้ว ตามโจทย์ให้ทำการปิด LIFF ด้วย liff.closeWindow()
// 7. Close the LIFF window
liff.closeWindow();
เมื่อทำตามทุกขั้นตอนเสร็จ จะได้โค้ดออกมาหน้าตาแบบนี้
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover"
/>
<title>LINE MINI App - Simple Service Message</title>
<!-- 1. Import LIFF SDK -->
<script
charset="utf-8"
src="https://static.line-scdn.net/liff/edge/2/sdk.js"
></script>
</head>
<body>
<script>
async function main() {
// 2. Initialize LIFF app with your LIFF ID
await liff.init({ liffId: "{{LIFF_ID}}" });
// 3. Check if the user is logged in
if (!liff.isLoggedIn()) {
// If not logged in, redirect to LINE login
liff.login();
return;
}
// 4. Get the LIFF access token
const liffAccessToken = liff.getAccessToken();
// 5. Send a simple service message
const response = await fetch("/send", {
method: "POST",
headers: {
"x-liff-access-token": liffAccessToken,
},
});
// 6. Check the response status
if (response.ok) {
alert("Message sent successfully!");
} else {
alert("Failed to send service message.");
}
// 7. Close the LIFF window
liff.closeWindow();
}
main();
</script>
</body>
</html>
เมื่อทำทุกอย่างเสร็จแล้ว เราจะมาทดสอบกันว่าจากที่ทำมา จะได้ตามโจทย์หรือไม่ โดยที่ไม่ต้อง Deploy เพียงรันคำสั่งเพื่อเปิดเซิร์ฟเวอร์สำหรับพัฒนาบนเครื่อง
npx wrangler pages dev .

และเปิดให้ด้านนอกเข้าถึงได้ด้วยวิธีการ Tunnel ซึ่งมีหลายบริการให้เลือกใช้งาน เช่น Ngrok หรือ Cloudflare แต่ในบทความนี้จะใช้งานผ่าน Cloudflare Tunnel สามารถรันคำสั่งง่าย ๆ เลย (แทนด้วยพอร์ตที่ได้มาจากการรันก่อนหน้านี้)
npx cloudflared tunnel --url localhost:8788

เราจะได้ URL มา 1 ตัว ซึ่งให้เรานำ URL นี้ไปใส่ใน Endpoint URL ในแชนเนล LINE MINI App บน LINE Developers Console

จากนั้นให้เราคัดลอก LIFF URL ในแชนเนล Developing ไปส่งใน LINE หรือเปิดเพื่อสแกน QR Code เพื่อใช้งาน LINE MINI App บน LINE ได้ทันที

นี่เป็นเพียงการสาธิตการทำ LINE MINI App เพื่อการส่ง Service Message แบบง่าย ๆ ที่สามารถทำได้ในระยะเวลาไม่ถึง 1 ชั่วโมง ตามภารกิจของ LINE HACK 2025 ในปีนี้ ส่วนภารกิจของ LINE HACK ครั้งต่อไปจะเป็นอะไรนั้น ต้องรอติดตามกันต่อไป
메타데이터
- post_id
- 05e46ea5d6e4
- slug
- line-simple-service-message-tips-05e46ea5d6e4
- url
- https://medium.com/linedevth/line-simple-service-message-tips-05e46ea5d6e4
- canonical_url
- https://medium.com/linedevth/line-simple-service-message-tips-05e46ea5d6e4
- author_url
- https://medium.com/@iamprompt
- status
- ok
- fetched_at
- 2026-07-22 17:42:12