# Guide to LINE Webhooks: Features and Best Practices

LINE webhooks are how a LINE Official Account bot hears from its users. When someone adds the account as a friend, sends it a message, taps a postback button, or invites it into a group chat, the LINE Platform sends that event to your bot server. If you build on the LINE Messaging API, everything your bot reacts to starts as a webhook.

This guide covers how LINE webhooks are configured and signed, the request that checks your URL, the events your bot receives and the shape of their payloads, the tight response window and opt-in redelivery, and the best practices for running a LINE receiver in production.

## What are LINE webhooks?

Each Messaging API channel has one webhook URL. When an event happens, the LINE Platform sends an HTTPS `POST` to it with a JSON body holding the bot's user ID in `destination` and an array of event objects in `events`. One request can carry several events of different types from different users, for example a message from one person and a follow from another.

Every request is signed. LINE computes an HMAC-SHA256 of the raw request body with your channel secret, base64-encodes it, and sends it in the `x-line-signature` header. LINE is unusually strict about the raw body: any change before verification, including deserialising, re-escaping or reformatting the JSON, will make the signature fail.

LINE also gives you very little time to answer. A bot server that does not respond within 2 seconds is logged as a `request_timeout` error, so handlers that call other services before replying are a common source of failures.

## LINE webhook features

| Feature | Details |
| --- | --- |
| Configuration | LINE Developers Console, **Messaging API** tab, then enable **Use webhook**. Or `PUT /v2/bot/channel/webhook/endpoint` |
| Endpoints | One webhook URL per channel |
| URL requirements | HTTPS, with a certificate from a widely trusted CA. Self-signed certificates are not permitted |
| Signature header | `x-line-signature`. LINE says header name casing "may change without notice" |
| Scheme | HMAC-SHA256, standard base64 with padding |
| Signed content | The raw request body, nothing else |
| Secret | The channel secret, on the channel's **Basic settings** tab. Admin role required |
| Replay window | None. No timestamp header is sent |
| Connectivity check | A signed request with an empty `events` array, which must get a `200` |
| Response | Status `200`. No response within 2 seconds is a `request_timeout` |
| Redelivery | Off by default. Opt in per channel. Count and interval are not disclosed |
| Deduplication key | `webhookEventId` on each event |
| Source IPs | Not disclosed |

LINE does not publish its sending IP addresses and tells you to rely on the signature instead.

## Common events

These are the Messaging API event types. Route on each event's `type`.

| Type | Fires when |
| --- | --- |
| `message` | A user sends a message. `message.type` is `text`, `image`, `video`, `audio`, `file`, `location` or `sticker` |
| `messageEdited` | A user edits a message |
| `unsend` | A user unsends a message |
| `follow` | The account is added as a friend, or unblocked |
| `unfollow` | The account is blocked |
| `join` | The account joins a group chat or multi-person chat |
| `leave` | The account is removed from a group chat or leaves a multi-person chat |
| `memberJoined` | A user joins a group chat or multi-person chat the account is in |
| `memberLeft` | A user leaves a group chat or multi-person chat the account is in |
| `postback` | A user performs a postback action |
| `videoPlayComplete` | A user finishes watching a video sent with a `trackingId` |
| `beacon` | A user enters the range of a LINE Beacon |
| `accountLink` | A user links their LINE account with your service's account |
| `membership` | A user joins, renews or leaves a membership of the account |

Corporate options deliver through the same webhook. Module channels receive `module` (for both attach and detach, told apart by `module.type`), `activated`, `deactivated`, `botSuspended` and `botResumed`, and LINE notification messages send a `delivery` event when a message has been delivered. Most bots never see these.

Here is LINE's example request, trimmed to two events. LINE truncates the user IDs in its own examples.

```json
{
  "destination": "xxxxxxxxxx",
  "events": [
    {
      "type": "message",
      "message": {
        "type": "text",
        "id": "14353798921116",
        "text": "Hello, world"
      },
      "timestamp": 1625665242211,
      "source": {
        "type": "user",
        "userId": "U80696558e1aa831..."
      },
      "replyToken": "757913772c4646b784d4b7ce46d12671",
      "mode": "active",
      "webhookEventId": "01FZ74A0TDDPYRVKNK77XKC3ZR",
      "deliveryContext": {
        "isRedelivery": false
      }
    },
    {
      "type": "follow",
      "timestamp": 1625665242214,
      "source": {
        "type": "user",
        "userId": "Ufc729a925b3abef..."
      },
      "replyToken": "bb173f4d9cf64aed9d408ab4e36339ad",
      "mode": "active",
      "webhookEventId": "01FZ74ASS536FW97EX38NKCZQK",
      "deliveryContext": {
        "isRedelivery": false
      }
    }
  ]
}

```

Every event carries `type`, `mode`, `timestamp`, `webhookEventId` and `deliveryContext`. `timestamp` is the time of the event in milliseconds. `webhookEventId` is a ULID that identifies the event. `mode` is `active` or `standby`, and a `standby` event has no reply token, so the bot should not reply. `source` is a user, group or room, and LINE says it is "not always included". Properties without a value are left out rather than sent as `null`.

## Setting up LINE webhooks

1. In the [LINE Developers Console](https://developers.line.biz/console/), open the provider and the Messaging API channel for your bot.
2. On the **Basic settings** tab, copy the **Channel secret** and store it server-side, for example as `LINE_CHANNEL_SECRET`. You need the Admin role to see it.
3. On the **Messaging API** tab, click **Edit** under **Webhook URL**, enter your HTTPS endpoint and click **Update**.
4. Click **Verify**. LINE sends a signed request with no events, and shows **Success** if your endpoint returns `200`.
5. Enable **Use webhook**.
6. Optionally, enable **Webhook redelivery** on the same tab, after reading the note LINE shows about it.

The request sent by **Verify** looks like this:

```json
{"destination":"U8e742f61d673b39c7fff3cecb7536ef0","events":[]}

```

It is signed exactly like a normal delivery, so a receiver that verifies the signature and returns `200` passes without any special case. The one thing to avoid is treating an empty `events` array as an error. LINE says it "may send" such a request to confirm communication, so accept it at any time, not only during setup.

## Securing LINE webhooks

Read `x-line-signature` (matching the header name without regard to case), compute an HMAC-SHA256 of the raw request body with the channel secret, base64-encode it, and compare the two in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyLineSignature(rawBody, signatureHeader, channelSecret) {
  if (!signatureHeader || !channelSecret) return false;

  const expected = crypto
    .createHmac('sha256', channelSecret)
    .update(rawBody)
    .digest('base64');

  const received = Buffer.from(signatureHeader, 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/line', express.raw({ type: '*/*' }), (req, res) => {
  // req.get() matches header names case-insensitively.
  if (!verifyLineSignature(req.body, req.get('x-line-signature'), process.env.LINE_CHANNEL_SECRET)) {
    return res.sendStatus(401);
  }

  const { events = [] } = JSON.parse(req.body.toString('utf8'));
  // An empty events array is LINE checking the connection: answer 200.
  // Otherwise queue each event and return 200 well inside 2 seconds.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac
import os

def verify_line_signature(raw_body: bytes, signature_header, channel_secret: str) -> bool:
    if not signature_header or not channel_secret:
        return False
    expected = base64.b64encode(
        hmac.new(channel_secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
    )
    # Compare as bytes: compare_digest raises TypeError on a str with non-ASCII characters.
    return hmac.compare_digest(signature_header.encode("utf-8"), expected)

# In FastAPI: raw_body = await request.body()
# verify_line_signature(raw_body, request.headers.get("x-line-signature"),
#                       os.environ["LINE_CHANNEL_SECRET"])

```

The official LINE Messaging API SDKs include a signature validator (`validateSignature` in `@line/bot-sdk`, `SignatureValidator` in the Python SDK) if you would rather not write your own.

LINE publishes its own test vector. With the channel secret `8c570fa6dd201bb328f1c1eac23a96d8` and this exact body:

```
{"destination":"U8e742f61d673b39c7fff3cecb7536ef0","events":[]}

```

the signature is:

```
GhRKmvmHys4Pi8DxkF4+EayaH0OqtJtaZxgTD9fMDLs=

```

You can reproduce it with `openssl`. Use single quotes so the shell does not interpret escape characters:

```bash
echo -n '{"destination":"U8e742f61d673b39c7fff3cecb7536ef0","events":[]}' | openssl dgst -sha256 -hmac '8c570fa6dd201bb328f1c1eac23a96d8' -binary | openssl base64

```

> **Make LINE webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `x-line-signature` header, answers LINE well inside the 2-second window, and durably queues every message, follow and postback event before your bot sees it.

## LINE webhook limitations and pain points

### Two seconds to respond

**The Problem:** Requests show up as `request_timeout` errors in the console statistics, and some events are lost.

**Why It Happens:** LINE counts a response that takes longer than 2 seconds as a failure. A handler that calls an AI model, a database or the reply API before answering easily goes over. LINE also notes that the webhook "may have been successfully received" in a timeout, so your server can process an event that LINE logs as failed.

**Workarounds:**

* Verify, queue and return `200` before doing any work, which is what LINE recommends.
* Reply to users from the worker. A redelivered event keeps the same reply token, and LINE says it can be used "except in certain cases".
* Watch the error statistics in the console under **Statistics**.

**How Hookdeck Can Help:** Hookdeck answers LINE as soon as a request arrives and stores it, then delivers it to your bot with its own timeout and retries. Your bot's processing time no longer counts against LINE's two seconds.

### No retries unless you turn them on

**The Problem:** A short outage on your side loses every event sent during it.

**Why It Happens:** LINE only resends failed webhooks if **Webhook redelivery** is enabled on the channel, and it is off by default. Even when it is on, LINE does not disclose how many times or how often it retries, says redelivery "doesn't guarantee that webhooks will be reliably delivered", and may switch it off if redeliveries surge. LINE also warns that it "may suspend sending webhooks" to a bot server that fails to receive them for a long time.

**Workarounds:**

* Enable webhook redelivery, and make your handler safe for the duplicates it brings.
* Keep the receiver simple and highly available, so that it is rarely the thing that fails.

**How Hookdeck Can Help:** With Hookdeck in front, LINE delivers to an endpoint that is always available. Hookdeck then retries against your bot on a schedule you control and lets you replay any event, so a bad deploy no longer depends on LINE's undisclosed redelivery.

### Duplicates and out-of-order events

**The Problem:** The bot answers the same message twice, or handles an unfollow before the follow.

**Why It Happens:** LINE says the same event "may be sent to your bot server more than once" for reasons such as network routing problems, even without redelivery. Redelivered events can also arrive in a different order from the one in which they happened.

**Workarounds:**

* Store each `webhookEventId` and skip IDs you have already handled. A redelivered event keeps its ID; only `deliveryContext.isRedelivery` changes.
* Use each event's `timestamp` to put events back in order where it matters.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeated deliveries before they reach your bot, based on the fields you choose.

### Signatures that fail for no obvious reason

**The Problem:** Verification fails even though the code follows the docs.

**Why It Happens:** LINE lists the usual causes: a proxy or load balancer changing the request, a framework parsing or pretty-printing the JSON before you verify, escape sequences such as `\n` being interpreted, the body being decoded as something other than UTF-8, a channel secret from a different channel, or another Admin having reissued the secret. Reissuing a secret invalidates the old one at once, with no overlap.

**Workarounds:**

* Verify the raw bytes before any parsing.
* If one server handles several bots, pick the channel secret using `destination`, which is the bot's user ID.
* Agree within your team who may reissue the channel secret, and update the receiver at the same moment.

**How Hookdeck Can Help:** Hookdeck verifies the signature on the request exactly as LINE sent it and keeps the raw body, so you can compare what arrived with what your own code sees.

## Best practices

### Verify against the raw body

Mount `express.raw()` on the webhook route, use `Buffer.from(await request.arrayBuffer())` in a Next.js route handler, or `await request.body()` in FastAPI. Parse only after the signature matches.

### Handle every event in the array

Loop over `events` and handle each one on its own. One request can mix types and users, and an empty array is a valid request that still needs a `200`.

### Respect `mode`

In `standby` mode the event has no reply token and the bot should not send messages. This matters when the account uses module channels, where another channel may be active.

### Deduplicate on `webhookEventId`

Store the ID before processing and check it on every event. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Two seconds leaves no room for downstream calls. Verify, queue, return `200`, and reply from a worker. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Keep the channel secret with the channel

Note which channel each secret belongs to, and keep it out of client code. For `delivery` events on channels that use LINE Chat Plus, LINE says to verify with the Switcher Secret instead of the channel secret.

## Conclusion

LINE signs every webhook with a base64 HMAC-SHA256 of the raw body in `x-line-signature`, keyed with the channel secret. Verify the bytes exactly as received, accept the empty-events request that checks your URL, loop over every event, deduplicate on `webhookEventId`, and answer within two seconds, because LINE only retries if you have opted in to redelivery.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your bot, checks every `x-line-signature`, answers LINE straight away, and holds each event until your bot has processed it, so a slow handler or an outage costs latency instead of lost messages.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle LINE webhooks reliably in minutes.