チュートリアル

LINE Botの作り方 2026:Messaging API開発入門

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

LineBot.pro Team· 更新 19 分で読めます
LINE Botの作り方 2026:Messaging API開発入門

#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(応答 / プッシュ)────┘
  1. ユーザーがメッセージを送る、ボタンをタップする、または友だち追加します。
  2. LINEプラットフォームが、1件以上のイベントをWebhook URLへHTTPSでPOSTします。
  3. サーバーは署名を検証し、200を返してからイベントを処理します。
  4. 応答トークンを使う応答メッセージ(reply)、または後からユーザーIDを指定する**プッシュメッセージ(push)**で返します。

応答メッセージは月間メッセージ数にカウントされません。プッシュ、マルチキャスト、ブロードキャスト、ナローキャストは、受信者1人につき1通としてカウントされます。

#どのタイプのBotを作るか

タイプ仕組み向いている用途注意点
キーワード型メッセージ内の単語に一致したら用意した回答を送るよくある質問、営業時間、料金登録していない言い回しには反応しない
メニュー / ポストバック型ボタン、クイックリプライ、リッチメニューがポストバックデータをサーバーに送る予約、注文、段階的な入力最初の設計に手間がかかる
AI補助型言語モデルが自由入力に答える多様な質問、多言語対応自社の情報に限定し、人へのエスカレーションが必須
ハイブリッド型主な手続きはボタン、それ以外はAI、例外は人が対応実際のビジネスの多くテストするパターンが増える

以下のコードは美容室向けのハイブリッド型です。予約はボタンで進め、AIは店舗のFAQだけを根拠に答え、「スタッフ」ボタンで人に切り替えます。

#事前準備とチャネル設定

#必要なもの

  • Messaging APIを有効にしたLINE公式アカウント(作成は無料)
  • Node.js 22以降(@line/bot-sdk v11の要件)またはPython 3.10以降(line-bot-sdk v3)
  • HTTPSで到達できるサーバー。ローカルで試す場合はngrokやCloudflare Tunnelなどのトンネル

#ステップ1:LINE公式アカウントを作り、Messaging APIを有効にする

2024年9月4日以降、LINE DevelopersコンソールでMessaging APIチャネルを直接作成することはできません。次の手順で作ります。

  1. LINE公式アカウントを作成します(ビジネスIDに登録して申込フォームに入力)。詳しくはLINE公式アカウントの作り方をご覧ください。
  2. LINE Official Account Managerで設定 → Messaging APIを開き、Messaging APIを有効にします。
  3. プロバイダーを選びます。プロバイダーは後から変更できず、同じユーザーでもプロバイダーごとに異なるユーザー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):

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

package.jsonにスクリプトを追加し、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

認証情報は.envファイルに書き、リポジトリにはコミットしないでください。Nodeは--env-fileで読み込みます。Pythonではシェルで環境変数をexportするか、ホスティング先のシークレット管理を使います。

bash
LINE_CHANNEL_SECRET=your_channel_secret
LINE_CHANNEL_ACCESS_TOKEN=your_channel_access_token
PORT=3000

#Webhookの実装:受信して返信する

#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("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)

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)  # 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件まで送れます。

ts
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ステップの予約、人への切り替えを実装しています。

ts
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に次を追加します。

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"選択した日付:{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つのルールです。

  1. 自社の情報だけを渡し、そこに答えがなければNO_ANSWERと返すよう指示します。
  2. 推測させずに人へ引き継ぎます。
  3. 応答トークンの期限内に収めます。 LINEは応答トークンを1分以内に使うよう案内しているため、モデルの呼び出しに上限(下の例では15秒)を設けます。それ以上かかる処理は「少々お待ちください」と返し、結果を後からプッシュメッセージで送ります。プッシュは月間メッセージ数にカウントされます。

この例ではGoogleのGen AI SDK(npm install @google/genai)を使います。モデルのバージョンは変わるため、モデル名は環境変数で指定し、Googleのドキュメントで最新のものを選んでください。

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 // 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のテキスト処理の最後の行を置き換えます。

ts
// 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イメージ

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"]

コンテナかNode.jsのプロセスをHTTPSの裏で動かせるホスティングなら、どこでも構いません。チャネルシークレット、アクセストークン、AIのキーは環境変数やシークレットとして渡し、イメージには含めないでください。

#LINEに本番のURLを設定する

  1. LINE DevelopersコンソールのMessaging API設定タブで、Webhook URLをhttps://your-domain/webhookにします。
  2. 検証をクリックします。LINEは空のイベント配列を送るので、サーバーは200を返す必要があります。
  3. Webhookの利用がオンになっていることを確認し、スマートフォンでテストします。

#重複、再送、ログ

  • Webhookの再送は初期状態でオフです。 コンソールでオンにすると、2xxを返さなかったWebhookをLINEが再送します。回数と間隔は公開されていません。
  • 再送をオンにしなくても重複は起こり得ます。 処理済みのwebhookEventIdのイベントはスキップしてください。再送されたイベントにはdeliveryContext.isRedelivery: trueが付きます。
  • ログはJSONの1行形式で、イベントIDを含めると1つの会話を追えます。
  • 上限を把握しておきます。 Messaging APIの多くのエンドポイントはチャネルごとに毎秒2,000リクエスト、マルチキャストは毎秒200、ブロードキャストとナローキャストは1時間に60リクエストまでです。429はレート制限か、月間のメッセージ上限に達したことを示します。
ts
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のトークのみを扱っています。


次のステップ:

LineBot.proについて

LineBot.proは、LINE公式アカウント向けのAIツールです。リッチメニュー、リッチメッセージ、バナーの作成、一斉配信、キーワードによる自動応答に対応しています。本記事はLINE Messaging APIの実務経験をもとに、当チームが執筆しています。

お問い合わせ
LineBot.pro

LINEビジネスを自動化する準備はできましたか?

LineBot.proでLINEコミュニケーションの自動化を今すぐ始めましょう。