Module 8: Webhook Events Deep Dive

Every event LINE sends to your server — message handling, user lifecycle, postbacks, and error strategies.

⏱ 25 min read 📖 Intermediate

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

EventTriggerHas replyToken?Key data
messageUser sends any messagemessage.type (text, image, sticker, etc.)
unsendUser unsends a messageunsend.messageId
followUser adds OA as friend
unfollowUser blocks/unfriends OA
joinOA is added to group/roomsource.type = group/room
leaveOA is removed from group/room
memberJoinedMember joins group/roomjoined.members[]
memberLeftMember leaves group/roomleft.members[]
postbackUser taps postback actionpostback.data, params (for datetime)
videoPlayCompleteUser finishes videovideoPlayComplete.trackingId
beaconBeacon enter/leave/bannerbeacon.hwid, beacon.type
accountLinkAccount linking resultlink.result, link.nonce
thingsLINE Things device eventthings.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 follow to send a flexible welcome sequence (not just a static greeting message)

8.6 Error Handling & Retry Strategy

HTTP StatusMeaningAction
200Success
400Bad request (invalid JSON, missing field)Fix your payload
401Invalid/expired access tokenRefresh token
403Cannot send (user blocked or left)Remove from active list
404Not found (invalid replyToken)Reply token expired (5 min)
409Conflict (already replied)Don't reply twice to same token
429Rate limitedExponential backoff + retry
5xxLINE server errorRetry 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-Signature header 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