Tutorial

LINE Chatbot Development Tutorial 2026: Node.js & Python

Build a LINE bot step by step with the Messaging API: enable the channel, verify webhook signatures, reply with Flex Messages and quick replies, run a booking flow with postbacks, add AI answers limited to your FAQ and deploy. Node.js (@line/bot-sdk v11) and Python (line-bot-sdk v3) code.

LineBot.pro Team· Updated 19 min read
LINE Chatbot Development Tutorial 2026: Node.js & Python

#How a LINE Bot Works

A LINE chatbot is a web service that receives webhook events from the LINE Platform and answers through the Messaging API. To build one in 2026 you need a LINE Official Account with the Messaging API enabled, an HTTPS endpoint that checks the x-line-signature header, and code that answers with the event's reply token. This tutorial builds a working bot in Node.js (TypeScript, @line/bot-sdk v11) and Python (line-bot-sdk v3): echo replies, buttons, a small booking flow, AI answers limited to your own FAQ, and a deployment checklist.

diagram
User ── message ──▶ LINE Platform ── webhook (POST + x-line-signature) ──▶ your server
                                                                              │
User ◀── answer ─── LINE Platform ◀──── Messaging API (reply or push) ────────┘
  1. A user sends a message, taps a button or adds your account as a friend.
  2. The LINE Platform sends an HTTPS POST with one or more events to your webhook URL.
  3. Your server verifies the signature, answers 200 and processes the events.
  4. It answers with the reply API (using the event's reply token) or later with push (using the user ID).

Reply messages don't count toward your plan's monthly message quota. Push, multicast, broadcast and narrowcast messages do, one message per recipient.

#Which kind of bot are you building?

TypeHow it worksGood forWatch out for
Keyword botMatches words in the message and sends a prepared answerFAQ, opening hours, pricesMisses phrasings you didn't list
Menu / postback botButtons, quick replies and a Rich Menu send postback data to your serverBookings, orders, step-by-step formsMore design work up front
AI-assisted botA language model answers free textVaried questions, several languagesMust be limited to your facts and needs a human fallback
HybridButtons for the main tasks, AI for the rest, a person for edge casesMost real businessesMore paths to test

The code below is a hybrid for a hair salon: buttons drive the booking, the AI only answers from the salon's FAQ, and a "Staff" button hands the chat to a person.

#Prerequisites and Channel Setup

#What you need

  • A LINE Official Account (free to create) with the Messaging API enabled
  • Node.js 22 or later (required by @line/bot-sdk v11) or Python 3.10 or later (line-bot-sdk v3)
  • A server reachable over HTTPS, and for local testing a tunnel such as ngrok or Cloudflare Tunnel

#Step 1: Create the LINE Official Account and enable the Messaging API

Since 4 September 2024 you can't create a Messaging API channel directly in the LINE Developers Console. Instead:

  1. Create a LINE Official Account: sign up for a Business ID and fill in the entry form. Our LINE Official Account setup guide walks through it.
  2. In LINE Official Account Manager, open Settings → Messaging API and enable the Messaging API.
  3. Choose a provider. Choose carefully: the provider can't be changed later, and the same user gets a different user ID under each provider, so keep all channels of one business under one provider.

The new Messaging API channel then appears in the LINE Developers Console.

#Step 2: Get your credentials

CredentialWhere to find itUsed for
Channel secretConsole → Basic settings tabVerifying webhook signatures
Channel access tokenConsole → Messaging API tab, or issued through the APICalling the Messaging API

LINE offers four kinds of channel access token. A long-lived token (no expiry, one per channel, issued in the console) is the quickest way to start testing. For production LINE recommends a channel access token v2.1 with a user-specified expiration (up to 30 days), which you request with a JSON Web Token signed by your own key. Short-lived (30 days) and stateless (15 minutes) tokens also exist.

#Step 3: Configure the channel

  • Webhook URL: your endpoint, for example https://bot.example.com/webhook. It must use HTTPS with a certificate from a widely trusted authority; self-signed certificates are rejected. A channel has exactly one webhook URL.
  • Click Verify, then turn on Use webhook.
  • In LINE Official Account Manager → Response settings, turn off the greeting message and auto-reply messages if your code handles them. Both are on by default and would answer alongside your bot. Keep Chat on if staff should be able to answer by hand.

#Step 4: Create the project

Node.js (TypeScript):

bash
mkdir line-bot && cd line-bot
npm init -y && npm pkg set type=module
npm install @line/bot-sdk express
npm install -D typescript tsx @types/express @types/node

Add the scripts to package.json and a tsconfig.json:

json
{
  "scripts": {
    "dev": "tsx watch --env-file=.env src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "skipLibCheck": true
  },
  "include": ["src"]
}

Python:

bash
mkdir line-bot && cd line-bot
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install line-bot-sdk flask

Put the credentials in a .env file and never commit it. The Node script loads it with --env-file; for Python, export the variables in your shell or use your host's secret manager.

bash
LINE_CHANNEL_SECRET=your_channel_secret
LINE_CHANNEL_ACCESS_TOKEN=your_channel_access_token
PORT=3000

#Webhook: Receive and Reply

#Node.js (TypeScript, @line/bot-sdk v11)

ts
// src/index.ts
import express from "express"
import {
  JSONParseError,
  LineBotClient,
  SignatureValidationFailed,
  middleware,
  webhook,
} from "@line/bot-sdk"

const channelSecret = process.env.LINE_CHANNEL_SECRET
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN
if (!channelSecret || !channelAccessToken) {
  throw new Error("Set LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN")
}

const client = LineBotClient.fromChannelAccessToken({ channelAccessToken })
const app = express()

// The LINE middleware checks x-line-signature against the raw body,
// so it must run before any JSON body parser.
app.post("/webhook", middleware({ channelSecret }), (req, res) => {
  res.sendStatus(200) // answer LINE first, then do the work
  const { events } = req.body as webhook.CallbackRequest
  for (const event of events) {
    handleEvent(event).catch((err) => console.error("event failed", err))
  }
})

app.use(express.json()) // other routes can parse JSON as usual

async function handleEvent(event: webhook.Event) {
  if (event.type !== "message" || event.message.type !== "text" || !event.replyToken) return

  await client.replyMessage({
    replyToken: event.replyToken,
    messages: [{ type: "text", text: `You said: ${event.message.text}` }],
  })
}

// The middleware hands errors to next(); without this handler Express answers 500.
app.use((err: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
  if (err instanceof SignatureValidationFailed) {
    res.status(401).send("Invalid signature")
    return
  }
  if (err instanceof JSONParseError) {
    res.status(400).send("Invalid JSON")
    return
  }
  next(err)
})

const port = Number(process.env.PORT ?? 3000)
app.listen(port, () => console.log(`LINE bot listening on :${port}`))

Start it with npm run dev, expose port 3000 through your tunnel, paste the HTTPS URL followed by /webhook into the console and click Verify. Then send your account a message from your phone: it echoes you.

What trips people up:

  • Middleware first. middleware() needs the raw body to recompute the HMAC-SHA256 signature. If express.json() runs before it, validation fails. Our @line/bot-sdk Express middleware guide covers the details.
  • The middleware doesn't send the 401 itself. It passes SignatureValidationFailed or JSONParseError to next(); without the error handler Express answers 500.
  • One request object per call. In SDK v11 every method takes a single object: replyMessage({ replyToken, messages }). The legacy Client class and client.replyMessage(token, message) were removed in v11.0.0 (April 2026).
  • Answer 200, then work. LINE recommends processing events asynchronously. Answering before processing is fine on a long-running server. On serverless platforms that stop the function once the response is sent, finish the work first or hand it to a queue.
  • Reply tokens are single-use, and LINE says to use them within one minute of receiving the webhook.
  • Empty event lists. LINE may send {"destination": "...", "events": []} to check the connection (the Verify button does). Answer 200.

#Python (line-bot-sdk v3, Flask)

python
# app.py
import os

from flask import Flask, abort, request
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
    ApiClient,
    Configuration,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage,
)
from linebot.v3.webhooks import MessageEvent, TextMessageContent

app = Flask(__name__)
configuration = Configuration(access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"])
handler = WebhookHandler(os.environ["LINE_CHANNEL_SECRET"])


@app.post("/webhook")
def webhook():
    signature = request.headers.get("X-Line-Signature", "")
    body = request.get_data(as_text=True)  # raw body, before any JSON parsing
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)
    return "OK"


@handler.add(MessageEvent, message=TextMessageContent)
def handle_text(event):
    with ApiClient(configuration) as api_client:
        MessagingApi(api_client).reply_message(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=f"You said: {event.message.text}")],
            )
        )


if __name__ == "__main__":
    app.run(port=int(os.environ.get("PORT", 3000)))

Run it with python app.py after exporting the two variables. WebhookHandler.handle() checks the signature against the raw body and raises InvalidSignatureError when it doesn't match. Import from linebot.v3: the 2.x LineBotApi interface is deprecated and only the linebot.v3 modules are maintained.

#Message Types and Buttons

Each of these is a message object you can pass in messages, up to five per reply or push:

ts
import { messagingApi } from "@line/bot-sdk"

const text: messagingApi.TextMessage = {
  type: "text",
  text: "Hello! How can we help today?", // up to 5,000 characters
}

const image: messagingApi.ImageMessage = {
  type: "image",
  originalContentUrl: "https://example.com/price-list.jpg", // HTTPS, JPEG or PNG, max 10 MB
  previewImageUrl: "https://example.com/price-list-preview.jpg", // max 1 MB
}

const card: messagingApi.FlexMessage = {
  type: "flex",
  altText: "Haircut – ฿450", // shown in notifications and the chat list
  contents: {
    type: "bubble",
    hero: {
      type: "image",
      url: "https://example.com/haircut.jpg",
      size: "full",
      aspectRatio: "20:13",
      aspectMode: "cover",
    },
    body: {
      type: "box",
      layout: "vertical",
      contents: [
        { type: "text", text: "Haircut", weight: "bold", size: "xl" },
        { type: "text", text: "฿450 · 45 minutes", color: "#06C755" },
      ],
    },
    footer: {
      type: "box",
      layout: "vertical",
      contents: [
        {
          type: "button",
          style: "primary",
          color: "#06C755",
          action: { type: "postback", label: "Book", data: "action=service&id=cut", displayText: "Book a haircut" },
        },
      ],
    },
  },
}

const menu: messagingApi.TextMessage = {
  type: "text",
  text: "What would you like to do?",
  quickReply: {
    items: [
      // up to 13 buttons
      { type: "action", action: { type: "postback", label: "Book", data: "action=start_booking", displayText: "Book" } },
      { type: "action", action: { type: "message", label: "Opening hours", text: "Opening hours" } },
      { type: "action", action: { type: "location", label: "Send my location" } },
    ],
  },
}
  • Text messages hold up to 5,000 characters.
  • Image URLs must use HTTPS. The original can be up to 10 MB (JPEG or PNG), the preview up to 1 MB.
  • Flex Messages are JSON layouts (a bubble or a carousel of bubbles). altText is what users see in notifications. Test layouts in LINE's Flex Message Simulator.
  • Quick replies show up to 13 buttons above the keyboard. Camera, camera roll and location actions only work in quick replies.
  • Postback actions send your server a postback event with up to 300 characters of data, without the user typing anything. displayText is what appears in the chat as the user's message.

Writing Flex JSON by hand is slow. If you'd rather start from a finished card, LineBot.pro's AI Rich Message generator produces a Flex bubble (hero image, text and a button) and lets you copy its JSON to send with the code above. For the permanent menu under the chat, see how to create a LINE Rich Menu.

#Conversation Flow: Postbacks, State and Handoff

A booking takes several steps, so the bot has to remember where each user is. This version of handleEvent replaces the echo version in src/index.ts (add messagingApi to the @line/bot-sdk import). It greets new friends, runs a three-step booking with postbacks and a date picker, and lets the user switch to a person:

ts
type Session =
  | { step: "idle" }
  | { step: "pick_date"; service: string }
  | { step: "confirm"; service: string; date: string }
  | { step: "human" } // a team member has taken over this chat

// In memory for the tutorial: it is lost on restart and isn't shared between
// instances. Use Redis or your database in production.
const sessions = new Map<string, Session>()

const SERVICES: Record<string, string> = { cut: "Haircut", color: "Hair color" }

async function handleEvent(event: webhook.Event) {
  // This tutorial handles 1:1 chats only.
  if (event.source?.type !== "user" || !event.source.userId) return
  const userId = event.source.userId

  if (event.type === "follow") {
    await reply(event.replyToken, [menuMessage("Welcome! Thanks for adding us as a friend.")])
    return
  }

  const session = sessions.get(userId) ?? { step: "idle" }
  if (session.step === "human") return // staff replies from LINE Official Account Manager

  if (event.type === "postback" && event.replyToken) {
    await onPostback(userId, session, event.postback, event.replyToken)
    return
  }

  if (event.type === "message" && event.replyToken) {
    if (event.message.type !== "text") {
      await reply(event.replyToken, [menuMessage("Thanks! I can only read text messages.")])
      return
    }
    const text = event.message.text.trim()
    if (["staff", "human"].includes(text.toLowerCase())) {
      sessions.set(userId, { step: "human" })
      await reply(event.replyToken, [
        { type: "text", text: "OK! A team member will answer in this chat during opening hours (10:00-20:00)." },
      ])
      return
    }
    await reply(event.replyToken, [menuMessage("Please choose an option below.")]) // AI section replaces this line
  }
}

async function onPostback(userId: string, session: Session, postback: webhook.PostbackContent, replyToken: string) {
  const data = new URLSearchParams(postback.data)
  const action = data.get("action")

  if (action === "start_booking") {
    await reply(replyToken, [{
      type: "text",
      text: "Which service would you like?",
      quickReply: {
        items: Object.entries(SERVICES).map(([id, label]) => ({
          type: "action" as const,
          action: { type: "postback" as const, label, data: `action=service&id=${id}`, displayText: label },
        })),
      },
    }])
    return
  }

  if (action === "service") {
    const service = SERVICES[data.get("id") ?? ""]
    if (!service) return
    sessions.set(userId, { step: "pick_date", service })
    await reply(replyToken, [{
      type: "text",
      text: `${service}: which day suits you?`,
      quickReply: {
        items: [{ type: "action", action: { type: "datetimepicker", label: "Pick a date", data: "action=date", mode: "date" } }],
      },
    }])
    return
  }

  if (action === "date" && session.step === "pick_date" && postback.params?.date) {
    const next: Session = { step: "confirm", service: session.service, date: postback.params.date }
    sessions.set(userId, next)
    await reply(replyToken, [{
      type: "text",
      text: `${next.service} on ${next.date}. Shall I send the request?`,
      quickReply: {
        items: [
          { type: "action", action: { type: "postback", label: "Confirm", data: "action=confirm", displayText: "Confirm" } },
          { type: "action", action: { type: "postback", label: "Cancel", data: "action=cancel", displayText: "Cancel" } },
        ],
      },
    }])
    return
  }

  if (action === "confirm" && session.step === "confirm") {
    sessions.delete(userId)
    // Save the request in your booking system or notify your team here.
    await reply(replyToken, [
      { type: "text", text: `Request received: ${session.service} on ${session.date}. We'll confirm the time in this chat.` },
    ])
    return
  }

  if (action === "cancel") {
    sessions.delete(userId)
    await reply(replyToken, [menuMessage("Cancelled. Anything else?")])
  }
}

function menuMessage(text: string): messagingApi.TextMessage {
  return {
    type: "text",
    text,
    quickReply: {
      items: [
        { type: "action", action: { type: "postback", label: "Book", data: "action=start_booking", displayText: "Book" } },
        { type: "action", action: { type: "message", label: "Staff", text: "staff" } },
      ],
    },
  }
}

async function reply(replyToken: string, messages: messagingApi.Message[]) {
  await client.replyMessage({ replyToken, messages })
}

What this shows:

  • Postback data is your protocol. action=service&id=cut is parsed with URLSearchParams. Keep it short (300 characters at most) and never trust it blindly: check that the step and the values make sense.
  • The date picker is a datetimepicker action; the chosen date comes back in postback.params.date (YYYY-MM-DD in date mode).
  • Every event type gets an answer. Stickers, photos and locations lead back to the menu instead of silence or a crash. Group chats have their own source types and are skipped here.
  • Store sessions outside memory in production, keyed by user ID, with an expiry for abandoned bookings.
  • Human handoff. After "Staff", the bot stays quiet for that user and your team answers in LINE Official Account Manager's chat (keep Chat on in Response settings). Give staff a way to hand the chat back: an admin command, a button in your back office or a timeout.
  • Keep the main actions in a Rich Menu so users rarely need to type.

The same postback handling in Python (line-bot-sdk v3), added to app.py:

python
from urllib.parse import parse_qsl

from linebot.v3.webhooks import PostbackEvent


@handler.add(PostbackEvent)
def handle_postback(event):
    data = dict(parse_qsl(event.postback.data))  # "action=service&id=cut"
    if data.get("action") == "date" and event.postback.params:
        text = f"Date chosen: {event.postback.params['date']}"
    else:
        text = f"Action: {data.get('action')}"
    with ApiClient(configuration) as api_client:
        MessagingApi(api_client).reply_message(
            ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=text)])
        )

#Adding AI Answers (Gemini Example)

A language model helps with the questions your buttons don't cover ("Do you cut children's hair?", "Is there parking?"). The risk is that it invents prices or promises. Three rules keep it safe:

  1. Give it only your facts, and tell it to answer NO_ANSWER when they don't cover the question.
  2. Hand over to a person instead of guessing.
  3. Stay inside the reply window. LINE says to use reply tokens within one minute, so cap the model call (15 seconds below). If a task takes longer, reply "One moment" and send the result later as a push message, which counts toward your monthly quota.

This example uses Google's Gen AI SDK (npm install @google/genai). The model name comes from an environment variable because model versions change; pick a current one from Google's documentation.

ts
// src/ai.ts
import { GoogleGenAI } from "@google/genai"

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY })
const model = process.env.GEMINI_MODEL // a current model name from Google's documentation
if (!model) throw new Error("Set GEMINI_MODEL")

// Only facts you are happy for the bot to repeat word for word.
const FAQ = `
Opening hours: 10:00-20:00, closed on Wednesdays.
Address: 12 Sukhumvit Soi 24, Bangkok (BTS Phrom Phong).
Haircut: ฿450, 45 minutes. Hair color: from ฿1,500, about 2 hours.
Booking: tap "Book" in the menu. Changes are possible up to 3 hours before.
`

export async function answerFromFaq(question: string): Promise<string | null> {
  const response = await ai.models.generateContent({
    model: model!,
    contents: question,
    config: {
      systemInstruction:
        "You answer customer questions for a hair salon using ONLY the FAQ below. " +
        "If the FAQ doesn't contain the answer, reply exactly NO_ANSWER. " +
        "Answer in the customer's language, in at most 3 short sentences. Never invent prices, times or promotions.\n\n" +
        `FAQ:\n${FAQ}`,
      temperature: 0.2,
      maxOutputTokens: 300,
      abortSignal: AbortSignal.timeout(15_000), // stay well inside the reply token's time limit
    },
  })
  const text = response.text?.trim()
  if (!text || text.includes("NO_ANSWER")) return null
  return text.slice(0, 5000) // a LINE text message holds up to 5,000 characters
}

Then, in handleEvent, replace the last line of the text branch:

ts
// at the top of src/index.ts (ES modules need the .js extension)
import { answerFromFaq } from "./ai.js"

// in handleEvent, instead of always sending the menu for free text:
await client.showLoadingAnimation({ chatId: userId, loadingSeconds: 20 })
const answer = await answerFromFaq(text).catch(() => null)
await reply(event.replyToken, [
  answer
    ? { type: "text", text: answer }
    : menuMessage("I'm not sure about that one. Tap Staff to reach a team member."),
])

The loading animation shows a typing indicator in 1:1 chats for 5 to 60 seconds (in steps of 5) and disappears as soon as your answer arrives.

Privacy: messages you pass to an AI provider leave LINE. Mention it in your privacy policy, don't forward personal data the model doesn't need, and check the provider's terms on how it uses data.

#Deployment and Monitoring

#Docker image

dockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

Any host that runs a container or a Node.js process behind HTTPS works. Pass the channel secret, the access token and the AI key as environment variables or secrets, never inside the image.

#Point LINE at the production URL

  1. In the LINE Developers Console → Messaging API tab, set the webhook URL to https://your-domain/webhook.
  2. Click Verify. LINE sends an empty event list, and your server must answer 200.
  3. Check that Use webhook is on, then test from a phone.

#Duplicates, retries and logs

  • Webhook redelivery is off by default. If you turn it on in the console, LINE re-sends webhooks that didn't get a 2xx answer. LINE doesn't disclose how many times or how often.
  • Duplicates can happen anyway. Skip events whose webhookEventId you've already handled. Redelivered events carry deliveryContext.isRedelivery: true.
  • Log JSON lines with the event ID so you can follow one conversation.
  • Know the limits. Most Messaging API endpoints allow 2,000 requests per second per channel, multicast 200 per second, broadcast and narrowcast 60 per hour. A 429 means a rate limit or your monthly message limit.
ts
import { HTTPFetchError } from "@line/bot-sdk"

const seen = new Map<string, number>() // webhookEventId -> time received. Use Redis (SET NX + TTL) with several instances.

function firstDelivery(event: webhook.Event): boolean {
  const now = Date.now()
  for (const [id, at] of seen) if (now - at > 30 * 60_000) seen.delete(id)
  if (seen.has(event.webhookEventId)) return false
  seen.set(event.webhookEventId, now)
  return true
}

function log(level: "info" | "error", msg: string, data: Record<string, unknown> = {}) {
  console.log(JSON.stringify({ time: new Date().toISOString(), level, msg, ...data }))
}

app.post("/webhook", middleware({ channelSecret }), (req, res) => {
  res.sendStatus(200)
  const { events } = req.body as webhook.CallbackRequest
  for (const event of events) {
    if (!firstDelivery(event)) continue // same event delivered twice
    handleEvent(event).catch((err) => {
      if (err instanceof HTTPFetchError) {
        // 429: rate limit or monthly message limit; 400 "Invalid reply token": used or too old
        log("error", "LINE API error", { status: err.status, body: err.body, eventId: event.webhookEventId })
      } else {
        log("error", "event failed", { error: String(err), eventId: event.webhookEventId })
      }
    })
  }
})

#Go-live checklist

  • A request with a wrong x-line-signature is rejected
  • Stickers, photos and locations get a sensible answer
  • The Staff handoff works, and staff can hand the chat back
  • The greeting and auto-reply settings in LINE Official Account Manager don't answer twice
  • Secrets live in environment variables, not in the repository
  • An alert fires on repeated 4xx or 5xx answers from the Messaging API

#LineBot.pro: the No-Code Route

If you came here because you want a bot rather than because you want to write one, there is a shorter path. LineBot.pro is not an SDK, an API or a hosting platform; it is a set of AI tools for LINE Official Accounts. The part that overlaps with this tutorial is the keyword bot in Automation:

  • 1–8 intents with 1–15 keywords each, every intent answering with a Rich Message card you approved
  • Keyword replies are free. Optional AI intent routing looks at messages that match no keyword (1 credit per message checked) and can only pick one of your approved cards: the AI never writes answers
  • Human-handoff keywords pause the bot for that customer
  • 1:1 chats only, with a free simulator to test before going live

It connects by pasting LineBot.pro's webhook URL into your channel. A channel has only one webhook URL, so it can't run alongside your own webhook server on the same channel.

Developers can still use the generators on their own: a Rich Message costs 8 credits and a Rich Menu 10 credits; you download the image and JSON, then send or upload them yourself. New accounts get 50 free credits; after that, credits come in one-time packs with no subscription and never expire (pricing). The keyword bot is described on the LINE chatbot page.

Need a bot connected to your booking system, your CRM or a LIFF app? That is custom development: see LINE development services.

#Frequently Asked Questions

Q: Can I still use new Client() from @line/bot-sdk? No. The legacy Client was deprecated in v10.8.0 and removed in v11.0.0 on 3 April 2026. Use LineBotClient.fromChannelAccessToken() or messagingApi.MessagingApiClient, whose methods take one request object, such as replyMessage({ replyToken, messages }). Version 11 needs Node.js 22 or later.

Q: Do reply messages count toward my LINE message quota? No. Messages sent with a reply token aren't counted. Push, multicast, broadcast and narrowcast messages are, once per recipient. Plans and prices differ by country; see our LINE Official Account pricing guide.

Q: How long is a reply token valid? It works once, and LINE says to use it within one minute of receiving the webhook; later use isn't guaranteed and the limit may change. For slow work, show the loading animation, or reply at once and send the result as a push message.

Q: Why does the Verify button in the LINE Developers Console fail? Usually one of these: the URL isn't HTTPS with a trusted certificate, the path is wrong, a body parser runs before the signature check, the channel secret belongs to another channel or was reissued, or the server doesn't answer 200 to an empty event list.

Q: Does a LINE bot need AI? No. Many good bots are buttons, a Rich Menu and keyword answers. Add AI for free-text questions, limit it to your own facts and keep a person as the fallback.

Q: Can the bot work in group chats? Yes, once you turn on Allow bot to join group chats on the Messaging API tab (off by default). Events then carry a groupId, replies go to the whole group, and the loading animation isn't available there. This tutorial handles 1:1 chats only.


Next steps:

About LineBot.pro

LineBot.pro is an AI tool for LINE Official Accounts: it creates Rich Menus, Rich Messages and banners, sends broadcast campaigns and runs keyword auto-replies. Our team writes these guides based on hands-on work with the LINE Messaging API.

Contact us
LineBot.pro

Ready to Automate Your LINE Business?

Start automating your LINE communications with LineBot.pro today.