Module 8: Webhook Events Deep Dive
Every event LINE sends to your server — message handling, user lifecycle, postbacks, and error strategies.
8.1 The Webhook Request
Every time a user interacts with your OA, LINE sends an HTTP POST to your webhook URL with this structure:
{
"destination": "U12345...", // Your OA's user ID
"events": [
{
"type": "message",
"webhookEventId": "01H...", // unique event ID
"deliveryContext": { "isRedelivery": false },
"timestamp": 1690000000000, // Unix millis
"source": {
"type": "user", // "user" | "group" | "room"
"userId": "U4a8b..." // sender's user ID
},
"replyToken": "nHuy...", // use within 5 min
"message": { ... } // message content
}
]
}
Always respond with 200 OK as quickly as possible. LINE may retry if you take too long or return an error. The 5-minute reply window starts from when LINE receives your 200 OK, not from when the event was created.
8.2 All Event Types
| Event | Trigger | Has replyToken? | Key data |
|---|---|---|---|
message | User sends any message | ✓ | message.type (text, image, sticker, etc.) |
unsend | User unsends a message | ✗ | unsend.messageId |
follow | User adds OA as friend | ✓ | — |
unfollow | User blocks/unfriends OA | ✗ | — |
join | OA is added to group/room | ✓ | source.type = group/room |
leave | OA is removed from group/room | ✗ | — |
memberJoined | Member joins group/room | ✓ | joined.members[] |
memberLeft | Member leaves group/room | ✗ | left.members[] |
postback | User taps postback action | ✓ | postback.data, params (for datetime) |
videoPlayComplete | User finishes video | ✓ | videoPlayComplete.trackingId |
beacon | Beacon enter/leave/banner | ✓ | beacon.hwid, beacon.type |
accountLink | Account linking result | ✓ | link.result, link.nonce |
things | LINE Things device event | ✓ | things.deviceId, things.result |
8.3 Message Event — Handling Each Type
app.post('/webhook', (req, res) => {
for (const event of req.body.events) {
if (event.type === 'message') {
const msg = event.message;
switch (msg.type) {
case 'text':
handleText(event, msg.text);
break;
case 'image':
handleImage(event, msg.id); // msg.id to download
break;
case 'video':
handleVideo(event, msg.id);
break;
case 'audio':
handleAudio(event, msg.id);
break;
case 'file':
handleFile(event, msg.fileName, msg.fileSize);
break;
case 'location':
handleLocation(event, msg.title, msg.address, msg.latitude, msg.longitude);
break;
case 'sticker':
handleSticker(event, msg.packageId, msg.stickerId);
break;
}
}
}
res.status(200).send('OK');
});
Downloading User-Sent Content
For images, videos, audio, and files sent by the user, you can download them using the message ID:
GET https://api-data.line.me/v2/bot/message/{messageId}/content
Authorization: Bearer {CHANNEL_ACCESS_TOKEN}
// Returns binary content. Max 1 min to download.
// Images are auto-resized to max 2560x2560.
// For original quality, use GET .../content?original=true
8.4 Postback Event — The Backbone of Interactivity
Postback events are triggered when a user taps an action with type: "postback". This is how you build interactive menus, confirmations, and data capture.
// Simple postback
"action": {
"type": "postback",
"label": "Buy Now",
"data": "action=buy&product_id=123"
}
// Data picker postback
"action": {
"type": "postback",
"label": "Select Date",
"data": "action=book",
"inputOption": "openKeyboard", // "openKeyboard", "openVoice", "closeKeyboard"
"fillInText": "Booking on %date%" // display format
}
// Webhook event for postback:
{
"type": "postback",
"replyToken": "...",
"source": { "userId": "U4a8b..." },
"postback": {
"data": "action=buy&product_id=123",
"params": {
"datetime": "2026-05-20T14:00", // if datetimepicker
"newRichMenuAliasId": "profile", // if richmenuswitch
"status": "success" // if richmenuswitch
}
}
}
Pro tip: Use URL-encoded key=value pairs in
data (like action=buy&product_id=123) — it's easy to parse. Always validate the data server-side — never trust user input.
8.5 Follow & Unfollow — User Lifecycle
// When user adds OA as friend
{
"type": "follow",
"replyToken": "...",
"source": { "userId": "U4a8b..." }
}
// When user blocks/unfriends OA — NOTE: no replyToken!
{
"type": "unfollow",
"source": { "userId": "U4a8b..." }
}
Best practices:
- On
follow: Send a welcome greeting, add user to your database, initialize their rich menu - On
unfollow: Mark user as inactive, stop sending pushes (or you'll get API errors) - Use
followto send a flexible welcome sequence (not just a static greeting message)
8.6 Error Handling & Retry Strategy
| HTTP Status | Meaning | Action |
|---|---|---|
| 200 | Success | — |
| 400 | Bad request (invalid JSON, missing field) | Fix your payload |
| 401 | Invalid/expired access token | Refresh token |
| 403 | Cannot send (user blocked or left) | Remove from active list |
| 404 | Not found (invalid replyToken) | Reply token expired (5 min) |
| 409 | Conflict (already replied) | Don't reply twice to same token |
| 429 | Rate limited | Exponential backoff + retry |
| 5xx | LINE server error | Retry with backoff |
Retry with Exponential Backoff (Node.js)
async function sendWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (err.response?.status === 429 || err.response?.status >= 500) {
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
await new Promise(r => setTimeout(r, delay));
continue;
}
throw err; // Non-retryable error
}
}
throw new Error('Max retries exceeded');
}
// Usage
await sendWithRetry(() => replyMessage(token, messages));
8.7 Webhook Rate Limits
- LINE sends events as they happen — there's no explicit rate limit on inbound webhooks
- But your API responses should be fast (< 200ms for 200 OK is ideal)
- If your server is slow, use a message queue (e.g., Redis/Bull, RabbitMQ) to process events asynchronously
// Async processing pattern
app.post('/webhook', (req, res) => {
res.status(200).send('OK'); // Respond immediately
// Process asynchronously
for (const event of req.body.events) {
queue.add({ event }); // e.g., Bull queue
}
});
8.8 Webhook Verification (Production Checklist)
- ☑ Validate
X-Line-Signatureheader on every request - ☑ Respond with 200 OK within 1 second
- ☑ Handle retries — LINE may redeliver events with
deliveryContext.isRedelivery: true - ☑ Use idempotency keys if processing payments/account actions
- ☑ Log all event types for debugging (even ones you don't process yet)
- ☑ Have a monitoring alert for HTTP 5xx responses to your webhook