LINE Botの作り方 2026:Messaging API開発入門
Messaging APIでLINE Botを作る手順を、チャネルの準備からWebhookの署名検証、Flexメッセージとクイックリプライ、ポストバックを使った予約フロー、FAQに限定したAI回答、デプロイまで解説します。Node.js(@line/bot-sdk v11)とPython(line-bot-sdk v3)のコード付きです。

#LINE Botの仕組み
LINE Bot(LINEのチャットボット)は、LINEプラットフォームから届くWebhookイベントを受け取り、Messaging APIで返信するWebサービスです。2026年に作るなら、Messaging APIを有効にしたLINE公式アカウント、x-line-signatureヘッダーを検証するHTTPSのエンドポイント、イベントの応答トークン(reply token)で返信するコードの3つが必要です。このチュートリアルでは、Node.js(TypeScript、@line/bot-sdk v11)とPython(line-bot-sdk v3)で、オウム返し、ボタン、簡単な予約フロー、自社FAQだけに基づくAI回答、デプロイ前のチェックまでを順に実装します。
ユーザー ── メッセージ ──▶ LINEプラットフォーム ── Webhook(POST + x-line-signature)──▶ あなたのサーバー
│
ユーザー ◀──── 返信 ────── LINEプラットフォーム ◀──── Messaging API(応答 / プッシュ)────┘
- ユーザーがメッセージを送る、ボタンをタップする、または友だち追加します。
- LINEプラットフォームが、1件以上のイベントをWebhook URLへHTTPSでPOSTします。
- サーバーは署名を検証し、
200を返してからイベントを処理します。 - 応答トークンを使う応答メッセージ(reply)、または後からユーザーIDを指定する**プッシュメッセージ(push)**で返します。
応答メッセージは月間メッセージ数にカウントされません。プッシュ、マルチキャスト、ブロードキャスト、ナローキャストは、受信者1人につき1通としてカウントされます。
#どのタイプのBotを作るか
| タイプ | 仕組み | 向いている用途 | 注意点 |
|---|---|---|---|
| キーワード型 | メッセージ内の単語に一致したら用意した回答を送る | よくある質問、営業時間、料金 | 登録していない言い回しには反応しない |
| メニュー / ポストバック型 | ボタン、クイックリプライ、リッチメニューがポストバックデータをサーバーに送る | 予約、注文、段階的な入力 | 最初の設計に手間がかかる |
| AI補助型 | 言語モデルが自由入力に答える | 多様な質問、多言語対応 | 自社の情報に限定し、人へのエスカレーションが必須 |
| ハイブリッド型 | 主な手続きはボタン、それ以外はAI、例外は人が対応 | 実際のビジネスの多く | テストするパターンが増える |
以下のコードは美容室向けのハイブリッド型です。予約はボタンで進め、AIは店舗のFAQだけを根拠に答え、「スタッフ」ボタンで人に切り替えます。
#事前準備とチャネル設定
#必要なもの
- Messaging APIを有効にしたLINE公式アカウント(作成は無料)
- Node.js 22以降(
@line/bot-sdkv11の要件)またはPython 3.10以降(line-bot-sdkv3) - HTTPSで到達できるサーバー。ローカルで試す場合はngrokやCloudflare Tunnelなどのトンネル
#ステップ1:LINE公式アカウントを作り、Messaging APIを有効にする
2024年9月4日以降、LINE DevelopersコンソールでMessaging APIチャネルを直接作成することはできません。次の手順で作ります。
- LINE公式アカウントを作成します(ビジネスIDに登録して申込フォームに入力)。詳しくはLINE公式アカウントの作り方をご覧ください。
- LINE Official Account Managerで設定 → Messaging APIを開き、Messaging APIを有効にします。
- プロバイダーを選びます。プロバイダーは後から変更できず、同じユーザーでもプロバイダーごとに異なるユーザーIDになるため、同じ事業のチャネルは1つのプロバイダーにまとめてください。
作成されたMessaging APIチャネルはLINE Developersコンソールに表示されます。
#ステップ2:認証情報を確認する
| 認証情報 | 確認場所 | 用途 |
|---|---|---|
| チャネルシークレット | コンソールのチャネル基本設定タブ | Webhook署名の検証 |
| チャネルアクセストークン | コンソールのMessaging API設定タブ、またはAPIで発行 | Messaging APIの呼び出し |
チャネルアクセストークンは4種類あります。テストを始めるには、コンソールで発行できる長期のチャネルアクセストークン(有効期限なし、1チャネルに1つ)が手軽です。本番環境についてLINEは、有効期間を指定できるチャネルアクセストークンv2.1(最長30日)を推奨しています。これは自分の鍵で署名したJSON Web Tokenを使って発行します。ほかに短期(30日)とステートレス(15分)のトークンがあります。
#ステップ3:チャネルを設定する
- Webhook URL:エンドポイント(例:
https://bot.example.com/webhook)。一般的なブラウザが信頼する認証局のSSL/TLS証明書を使ったHTTPSが必須で、自己署名証明書は使えません。1つのチャネルに設定できるWebhook URLは1つだけです。 - 検証をクリックし、Webhookの利用をオンにします。
- コードで処理する場合は、LINE Official Account Managerの応答設定であいさつメッセージと応答メッセージ(管理画面の自動応答)をオフにします。どちらも初期状態でオンのため、Botと二重に返信してしまいます。スタッフが手動で返信する場合はチャットをオンのままにします。
#ステップ4:プロジェクトを作る
Node.js(TypeScript):
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
package.jsonにスクリプトを追加し、tsconfig.jsonを作ります。
{
"scripts": {
"dev": "tsx watch --env-file=.env src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src"]
}
Python:
mkdir line-bot && cd line-bot
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install line-bot-sdk flask
認証情報は.envファイルに書き、リポジトリにはコミットしないでください。Nodeは--env-fileで読み込みます。Pythonではシェルで環境変数をexportするか、ホスティング先のシークレット管理を使います。
LINE_CHANNEL_SECRET=your_channel_secret
LINE_CHANNEL_ACCESS_TOKEN=your_channel_access_token
PORT=3000
#Webhookの実装:受信して返信する
#Node.js(TypeScript、@line/bot-sdk v11)
// 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("LINE_CHANNEL_SECRET と LINE_CHANNEL_ACCESS_TOKEN を設定してください")
}
const client = LineBotClient.fromChannelAccessToken({ channelAccessToken })
const app = express()
// LINEのミドルウェアは生のリクエストボディでx-line-signatureを検証するため、
// JSONのボディパーサーより前に置きます。
app.post("/webhook", middleware({ channelSecret }), (req, res) => {
res.sendStatus(200) // 先にLINEへ200を返してから処理する
const { events } = req.body as webhook.CallbackRequest
for (const event of events) {
handleEvent(event).catch((err) => console.error("イベント処理に失敗", err))
}
})
app.use(express.json()) // ほかのルートでは通常どおりJSONを解析できる
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: `「${event.message.text}」ですね` }],
})
}
// ミドルウェアはエラーをnext()に渡すだけ。このハンドラーがないとExpressは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がポート${port}で待機中`))
npm run devで起動し、トンネルでポート3000を公開して、HTTPSのURLに/webhookを付けてコンソールに貼り、検証をクリックします。スマートフォンから友だち追加したアカウントにメッセージを送ると、オウム返しが届きます。
つまずきやすい点:
- ミドルウェアを先に置く。
middleware()はHMAC-SHA256の署名を再計算するために生のボディを必要とします。express.json()が先に動くと検証に失敗します。詳しくは@line/bot-sdk Expressミドルウェア解説をご覧ください。 - ミドルウェア自身は401を返さない。
SignatureValidationFailedやJSONParseErrorをnext()に渡すだけなので、エラーハンドラーがないとExpressは500を返します。 - メソッドの引数はリクエストオブジェクト1つ。 SDK v11では
replyMessage({ replyToken, messages })のように1つのオブジェクトを渡します。従来のClientクラスとclient.replyMessage(token, message)はv11.0.0(2026年4月)で削除されました。 - 先に200を返して処理する。 LINEはイベントの非同期処理を推奨しています。常駐するサーバーなら返信前に200を返して問題ありません。レスポンス送信後に関数を止めるサーバーレス環境では、処理を終えてから返すか、キューに渡してください。
- 応答トークンは1回限りで、LINEはWebhookの受信から1分以内に使うよう案内しています。
- 空のイベント配列。 接続確認のために
{"destination": "...", "events": []}が届くことがあります(検証ボタンもこれを送ります)。200を返してください。
#Python(line-bot-sdk v3、Flask)
# 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) # JSONとして解析する前の生のボディ
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"「{event.message.text}」ですね")],
)
)
if __name__ == "__main__":
app.run(port=int(os.environ.get("PORT", 3000)))
2つの環境変数をexportしてからpython app.pyで起動します。WebhookHandler.handle()は生のボディで署名を検証し、一致しなければInvalidSignatureErrorを送出します。インポートはlinebot.v3から行ってください。2.x系のLineBotApiは非推奨で、メンテナンスされているのはlinebot.v3モジュールだけです。
#メッセージの種類とボタン
以下はどれもmessagesに渡せるメッセージオブジェクトで、1回の応答・プッシュで最大5件まで送れます。
import { messagingApi } from "@line/bot-sdk"
const text: messagingApi.TextMessage = {
type: "text",
text: "こんにちは!ご用件をお聞かせください。", // 最大5,000文字
}
const image: messagingApi.ImageMessage = {
type: "image",
originalContentUrl: "https://example.com/price-list.jpg", // HTTPS、JPEGまたはPNG、最大10MB
previewImageUrl: "https://example.com/price-list-preview.jpg", // 最大1MB
}
const card: messagingApi.FlexMessage = {
type: "flex",
altText: "カット 4,400円", // 通知やトークリストに表示される
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: "カット", weight: "bold", size: "xl" },
{ type: "text", text: "4,400円・45分", color: "#06C755" },
],
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#06C755",
action: { type: "postback", label: "予約する", data: "action=service&id=cut", displayText: "カットを予約したい" },
},
],
},
},
}
const menu: messagingApi.TextMessage = {
type: "text",
text: "ご希望の内容を選んでください。",
quickReply: {
items: [
// ボタンは最大13個
{ type: "action", action: { type: "postback", label: "予約", data: "action=start_booking", displayText: "予約" } },
{ type: "action", action: { type: "message", label: "営業時間", text: "営業時間" } },
{ type: "action", action: { type: "location", label: "位置情報を送る" } },
],
},
}
- テキストは最大5,000文字です。
- 画像のURLはHTTPS必須です。元画像は最大10MB(JPEGまたはPNG)、プレビュー画像は最大1MBです。
- FlexメッセージはJSONで組むレイアウト(バブル、またはバブルを並べたカルーセル)です。
altTextは通知に表示される文言です。レイアウトはLINEのFlex Message Simulatorで確認できます。 - クイックリプライはキーボードの上に最大13個のボタンを表示します。カメラ、カメラロール、位置情報のアクションはクイックリプライでしか使えません。
- ポストバックアクションは、ユーザーが文字を打たなくても、最大300文字の
dataを含むpostbackイベントをサーバーに送ります。displayTextはユーザーの発言としてトークに表示される文言です。
FlexのJSONを手で書くのは手間がかかります。完成したカードから始めたい場合は、LineBot.proのAIリッチメッセージ作成でFlexバブル(ヒーロー画像、テキスト、ボタン1つ)を作り、そのJSONをコピーして上のコードで送れます。トーク画面の下に常に表示するメニューについてはLINEリッチメニューの作り方をご覧ください。
#会話フロー:ポストバック、状態管理、有人対応への切り替え
予約は複数のステップに分かれるため、Botはユーザーごとに「今どこまで進んだか」を覚えておく必要があります。次のhandleEventで、src/index.tsのオウム返し版を置き換えます(@line/bot-sdkのインポートにmessagingApiを追加してください)。新しい友だちへのあいさつ、ポストバックと日付選択による3ステップの予約、人への切り替えを実装しています。
type Session =
| { step: "idle" }
| { step: "pick_date"; service: string }
| { step: "confirm"; service: string; date: string }
| { step: "human" } // スタッフがこのトークを引き継いだ状態
// チュートリアルのためメモリに保存。再起動で消え、複数インスタンス間で共有されないため、
// 本番ではRedisやデータベースを使う。
const sessions = new Map<string, Session>()
const SERVICES: Record<string, string> = { cut: "カット", color: "カラー" }
async function handleEvent(event: webhook.Event) {
// このチュートリアルは1対1のトークのみ扱う
if (event.source?.type !== "user" || !event.source.userId) return
const userId = event.source.userId
if (event.type === "follow") {
await reply(event.replyToken, [menuMessage("友だち追加ありがとうございます!")])
return
}
const session = sessions.get(userId) ?? { step: "idle" }
if (session.step === "human") return // スタッフが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("ありがとうございます。テキストのメッセージのみ読み取れます。")])
return
}
const text = event.message.text.trim()
if (["staff", "スタッフ"].includes(text.toLowerCase())) {
sessions.set(userId, { step: "human" })
await reply(event.replyToken, [
{ type: "text", text: "承知しました。営業時間内(10:00〜20:00)にスタッフがこのトークで返信します。" },
])
return
}
await reply(event.replyToken, [menuMessage("下のボタンから選んでください。")]) // AIの章でこの行を置き換える
}
}
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: "どのメニューにしますか?",
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}ですね。ご希望の日を選んでください。`,
quickReply: {
items: [{ type: "action", action: { type: "datetimepicker", label: "日付を選ぶ", 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.date}に${next.service}で予約リクエストを送りますか?`,
quickReply: {
items: [
{ type: "action", action: { type: "postback", label: "送信する", data: "action=confirm", displayText: "送信する" } },
{ type: "action", action: { type: "postback", label: "やめる", data: "action=cancel", displayText: "やめる" } },
],
},
}])
return
}
if (action === "confirm" && session.step === "confirm") {
sessions.delete(userId)
// ここで予約システムに保存する、またはスタッフに通知する
await reply(replyToken, [
{ type: "text", text: `${session.date}の${session.service}のリクエストを受け付けました。時間はこのトークでご連絡します。` },
])
return
}
if (action === "cancel") {
sessions.delete(userId)
await reply(replyToken, [menuMessage("キャンセルしました。ほかにご用件はありますか?")])
}
}
function menuMessage(text: string): messagingApi.TextMessage {
return {
type: "text",
text,
quickReply: {
items: [
{ type: "action", action: { type: "postback", label: "予約", data: "action=start_booking", displayText: "予約" } },
{ type: "action", action: { type: "message", label: "スタッフと話す", text: "スタッフ" } },
],
},
}
}
async function reply(replyToken: string, messages: messagingApi.Message[]) {
await client.replyMessage({ replyToken, messages })
}
ポイント:
- ポストバックのデータは自分で決めるプロトコルです。
action=service&id=cutをURLSearchParamsで解析します。300文字以内に収め、内容をうのみにせず、ステップと値が妥当かを確認します。 - 日付選択は
datetimepickerアクションです。選ばれた日付はpostback.params.dateに入ります(dateモードではYYYY-MM-DD形式)。 - どのイベントにも反応を返します。 スタンプ、写真、位置情報が来ても、無反応やエラーにせずメニューへ戻します。グループトークは送信元の種類が異なるため、ここでは処理しません。
- 本番ではセッションをメモリ以外に保存します。 ユーザーIDをキーにし、途中で放置された予約には有効期限を付けます。
- 有人対応への切り替え。 「スタッフ」を押すとBotはそのユーザーに返信しなくなり、スタッフがLINE Official Account Managerのチャットで答えます(応答設定でチャットをオンにしておきます)。管理用コマンド、社内画面のボタン、タイムアウトなど、Botに戻す手段も用意してください。
- 主な操作はリッチメニューに置き、ユーザーが文字を打たずに済むようにします。
Python(line-bot-sdk v3)で同じポストバック処理を書く場合は、app.pyに次を追加します。
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"選択した日付:{event.postback.params['date']}"
else:
text = f"アクション:{data.get('action')}"
with ApiClient(configuration) as api_client:
MessagingApi(api_client).reply_message(
ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=text)])
)
#AIによる回答を追加する(Geminiの例)
言語モデルは、ボタンでは拾えない質問(「子どものカットはできますか?」「駐車場はありますか?」)に役立ちます。ただし料金や約束をでっち上げる危険があります。安全に使うための3つのルールです。
- 自社の情報だけを渡し、そこに答えがなければ
NO_ANSWERと返すよう指示します。 - 推測させずに人へ引き継ぎます。
- 応答トークンの期限内に収めます。 LINEは応答トークンを1分以内に使うよう案内しているため、モデルの呼び出しに上限(下の例では15秒)を設けます。それ以上かかる処理は「少々お待ちください」と返し、結果を後からプッシュメッセージで送ります。プッシュは月間メッセージ数にカウントされます。
この例ではGoogleのGen AI SDK(npm install @google/genai)を使います。モデルのバージョンは変わるため、モデル名は環境変数で指定し、Googleのドキュメントで最新のものを選んでください。
// src/ai.ts
import { GoogleGenAI } from "@google/genai"
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY })
const model = process.env.GEMINI_MODEL // Googleのドキュメントに載っている現行のモデル名
if (!model) throw new Error("GEMINI_MODEL を設定してください")
// Botがそのまま繰り返してもよい情報だけを書く
const FAQ = `
営業時間:10:00〜20:00、毎週水曜定休。
住所:東京都渋谷区神宮前1-2-3(JR原宿駅から徒歩5分)。
カット:4,400円(45分)。カラー:6,600円〜(約2時間)。
予約:メニューの「予約」をタップ。変更は3時間前まで。
`
export async function answerFromFaq(question: string): Promise<string | null> {
const response = await ai.models.generateContent({
model: model!,
contents: question,
config: {
systemInstruction:
"あなたは美容室の問い合わせに答えるアシスタントです。下のFAQだけを根拠に答えてください。" +
"FAQに答えがない場合は、NO_ANSWER とだけ返してください。" +
"お客様と同じ言語で、3文以内で簡潔に答えてください。料金、時間、キャンペーンを作り出してはいけません。\n\n" +
`FAQ:\n${FAQ}`,
temperature: 0.2,
maxOutputTokens: 300,
abortSignal: AbortSignal.timeout(15_000), // 応答トークンの期限に十分間に合うように
},
})
const text = response.text?.trim()
if (!text || text.includes("NO_ANSWER")) return null
return text.slice(0, 5000) // LINEのテキストメッセージは最大5,000文字
}
続いてhandleEventのテキスト処理の最後の行を置き換えます。
// src/index.ts の先頭(ES Modulesでは拡張子 .js が必要)
import { answerFromFaq } from "./ai.js"
// handleEvent内で、自由入力に毎回メニューを返す代わりに:
await client.showLoadingAnimation({ chatId: userId, loadingSeconds: 20 })
const answer = await answerFromFaq(text).catch(() => null)
await reply(event.replyToken, [
answer
? { type: "text", text: answer }
: menuMessage("その件はお答えできません。「スタッフと話す」を押すと担当者につながります。"),
])
ローディングアニメーションは1対1のトークで5〜60秒(5秒単位)表示され、返信が届いた時点で消えます。
プライバシー:AIの提供元に渡したメッセージはLINEの外に出ます。プライバシーポリシーに明記し、モデルに不要な個人情報は渡さず、提供元のデータ利用条件を確認してください。
#デプロイと監視
#Dockerイメージ
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"]
コンテナかNode.jsのプロセスをHTTPSの裏で動かせるホスティングなら、どこでも構いません。チャネルシークレット、アクセストークン、AIのキーは環境変数やシークレットとして渡し、イメージには含めないでください。
#LINEに本番のURLを設定する
- LINE DevelopersコンソールのMessaging API設定タブで、Webhook URLを
https://your-domain/webhookにします。 - 検証をクリックします。LINEは空のイベント配列を送るので、サーバーは200を返す必要があります。
- Webhookの利用がオンになっていることを確認し、スマートフォンでテストします。
#重複、再送、ログ
- Webhookの再送は初期状態でオフです。 コンソールでオンにすると、2xxを返さなかったWebhookをLINEが再送します。回数と間隔は公開されていません。
- 再送をオンにしなくても重複は起こり得ます。 処理済みの
webhookEventIdのイベントはスキップしてください。再送されたイベントにはdeliveryContext.isRedelivery: trueが付きます。 - ログはJSONの1行形式で、イベントIDを含めると1つの会話を追えます。
- 上限を把握しておきます。 Messaging APIの多くのエンドポイントはチャネルごとに毎秒2,000リクエスト、マルチキャストは毎秒200、ブロードキャストとナローキャストは1時間に60リクエストまでです。
429はレート制限か、月間のメッセージ上限に達したことを示します。
import { HTTPFetchError } from "@line/bot-sdk"
const seen = new Map<string, number>() // webhookEventId -> 受信時刻。複数インスタンスならRedis(SET NX + TTL)を使う
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 // 同じイベントが2回届いた
handleEvent(event).catch((err) => {
if (err instanceof HTTPFetchError) {
// 429:レート制限または月間上限、400「Invalid reply token」:使用済みか期限切れ
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 })
}
})
}
})
#公開前チェックリスト
- 誤った
x-line-signatureのリクエストが拒否される - スタンプ、写真、位置情報にも適切に反応する
- スタッフへの切り替えが機能し、Botに戻せる
- LINE Official Account Managerのあいさつメッセージ・応答メッセージと二重に返信しない
- シークレットはリポジトリではなく環境変数にある
- Messaging APIから4xx・5xxが続いたらアラートが届く
#LineBot.pro:コードを書かない方法
Botを「作りたい」のではなく「使いたい」のであれば、もっと短い道があります。LineBot.proはSDKやAPI、ホスティングサービスではなく、LINE公式アカウント向けのAIツールです。このチュートリアルと重なるのは、自動応答のキーワードBotです。
- 1〜8個のインテントに、それぞれ1〜15個のキーワードを設定し、各インテントは承認済みのリッチメッセージのカードで返信します
- キーワードでの返信は無料です。オプションのAIインテント振り分けは、どのキーワードにも一致しないメッセージを判定し(判定1件につき1クレジット)、承認済みのカードから1つを選ぶだけです。AIが回答文を書くことはありません
- 有人対応キーワードで、そのお客様に対するBotを一時停止できます
- 1対1のトークのみ対応。公開前に無料のシミュレーターで試せます
接続は、LineBot.proのWebhook URLをチャネルに貼り付けて行います。1つのチャネルに設定できるWebhook URLは1つだけなので、同じチャネルで自作のWebhookサーバーと同時には使えません。
開発者がジェネレーターだけを使うこともできます。リッチメッセージは8クレジット、リッチメニューは10クレジットで、画像とJSONをダウンロードし、自分のコードで送信またはアップロードします。新規登録で50クレジットが無料で付き、その後は買い切りのクレジットパックです。月額料金はなく、クレジットに有効期限はありません(料金)。キーワードBotの詳細はLINE チャットボットのページをご覧ください。
予約システム、顧客管理、LIFFアプリと連携したBotが必要な場合は個別開発になります。LINE開発サービスをご覧ください。
#よくある質問
Q: @line/bot-sdkのnew Client()はまだ使えますか?
使えません。従来のClientはv10.8.0で非推奨になり、2026年4月3日のv11.0.0で削除されました。LineBotClient.fromChannelAccessToken()かmessagingApi.MessagingApiClientを使い、replyMessage({ replyToken, messages })のようにリクエストオブジェクトを1つ渡します。v11にはNode.js 22以降が必要です。
Q: 応答メッセージは月間メッセージ数にカウントされますか? カウントされません。応答トークンで送ったメッセージは対象外です。プッシュ、マルチキャスト、ブロードキャスト、ナローキャストは受信者1人につき1通としてカウントされます。プランと料金は国ごとに異なるため、LINE公式アカウントの料金ガイドをご覧ください。
Q: 応答トークンの有効期限はどのくらいですか? 使えるのは1回だけで、LINEはWebhookの受信から1分以内に使うよう案内しています。それ以降の動作は保証されず、期限は変わる可能性があります。時間のかかる処理では、ローディングアニメーションを表示するか、すぐに返信して結果をプッシュメッセージで送ってください。
Q: LINE Developersコンソールの「検証」が失敗するのはなぜですか? 多い原因は、信頼された証明書のHTTPSになっていない、パスが違う、署名検証の前にボディパーサーが動いている、チャネルシークレットが別のチャネルのものか再発行済み、空のイベント配列に200を返していない、のいずれかです。
Q: LINE BotにAIは必要ですか? 必須ではありません。ボタン、リッチメニュー、キーワード応答だけでも十分役に立つBotは作れます。AIは自由入力の質問に使い、自社の情報に限定して、最後は人が対応できるようにしておきます。
Q: グループトークでも使えますか?
使えます。Messaging API設定タブでグループトーク・複数人トークへの参加を許可するをオンにしてください(初期状態はオフ)。イベントにgroupIdが含まれ、返信はグループ全体に届きます。ローディングアニメーションはグループでは使えません。このチュートリアルは1対1のトークのみを扱っています。
次のステップ:
- 署名検証と
replyMessageをさらに詳しく:@line/bot-sdk Expressミドルウェア解説 - LINEログイン、LIFF、HTTPでの直接呼び出し:LINE API連携チュートリアル
- Botに常設メニューを付ける:LINEリッチメニューの作り方
- 開発を任せたい場合:LINE開発サービス
- コードを書かずに始めたい場合:LINE チャットボットのキーワードBot
LineBot.proについて
LineBot.proは、LINE公式アカウント向けのAIツールです。リッチメニュー、リッチメッセージ、バナーの作成、一斉配信、キーワードによる自動応答に対応しています。本記事はLINE Messaging APIの実務経験をもとに、当チームが執筆しています。
お問い合わせ関連記事

@line/bot-sdk Expressミドルウェア実装例(v11)
@line/bot-sdk v11のExpress+TypeScriptによるWebhook実装例。middleware()を置く場所、Node.js・Python・PHPでのvalidateSignature、Flex Messageを使ったreplyMessage、sentMessagesと引用トークン、実際に出るエラーと対処を解説します。
続きを読む
LINE APIでできること・連携方法:Messaging API/LINEログイン
2026年時点のLINE APIの全体像と、SDKを使わずに直接呼び出す方法を開発者向けに解説します。各APIでできること、チャネルの準備、fetchでの応答・プッシュ送信、Webhookの署名検証、PKCE対応のLINEログイン、LIFFのIDトークン、レート制限とエラー処理まで。
続きを読む
LINEリッチメッセージとは?画像サイズ・作り方とカードタイプの違い
リッチメッセージ・カードタイプメッセージ・リッチビデオメッセージ・Flex Messageの違いを、公式の画像サイズ、管理画面での作り方、配信通数の数え方まで1ページで整理しました。
続きを読む