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.
{
"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
- In the LINE Developers Console, open the provider and the Messaging API channel for your bot.
- 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. - On the Messaging API tab, click Edit under Webhook URL, enter your HTTPS endpoint and click Update.
- Click Verify. LINE sends a signed request with no events, and shows Success if your endpoint returns
200. - Enable Use webhook.
- Optionally, enable Webhook redelivery on the same tab, after reading the note LINE shows about it.
The request sent by Verify looks like this:
{"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.
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:
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:
echo -n '{"destination":"U8e742f61d673b39c7fff3cecb7536ef0","events":[]}' | openssl dgst -sha256 -hmac '8c570fa6dd201bb328f1c1eac23a96d8' -binary | openssl base64
Make LINE webhooks production-ready. Hookdeck Event Gateway verifies the
x-line-signatureheader, 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
200before 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
webhookEventIdand skip IDs you have already handled. A redelivered event keeps its ID; onlydeliveryContext.isRedeliverychanges. - Use each event's
timestampto 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.
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.
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 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 for free and handle LINE webhooks reliably in minutes.