IT技術ブログフィットネス・健康

LINE自動返信の作り方業種別の文例とWebhookの線引き

LINEの自動返信を、管理画面の応答メッセージで足りる形とWebhookが要る形に分けて設計します。ジム・整体院・結婚式場・リフォーム会社の文例、返信が止まる設定の誤り、手元で動かしたキーワード応答のコードまで。

この記事の結論:LINEの自動返信は、あいさつ、キーワード応答、営業時間外の返信、人へ引き継ぐ線の4つに分けて設計します。決まった言葉と時間で返すだけなら、LINE Official Account Managerの応答メッセージで足ります。文の中の言葉で答えを選ぶ、相手の予約で答えを変える、人に渡した会話の自動返信を止める、件数を数える、のどれかが要るなら、Messaging APIとWebhookで作ります。どちらの場合も、先に決めるのは「自動で答えないこと」です。後半のコードは手元で動かしたもので、本物のLINEでは確かめていません。

この記事の内容のご相談は株式会社bundlyzeへbundlyzeのシステム開発について見てみる

この記事は、パーソナルジム、整体院、結婚式場、リフォーム会社で、公式LINEの問い合わせに手が回っていない担当者に向けて書きました。扱うのは、届いた問い合わせへの返信の設計と文例です。管理画面の操作を順に追いたい場合は会社のコラムLINE公式アカウントで問い合わせを受ける設定、配信や通数の話はLINE公式アカウントの料金、道具の選び方は拡張ツールで足りるか、自社で作るかが先です。

後半のコードは、2026年11月1日に手元のwrangler devと手元のD1で動かしたものです。LINEのAPIの代わりに、応答を真似る小さなサーバーを立てました。本物のLINEには一度も送っておらず、スマホの画面でも見ていません。 管理画面の項目名は、LINEヤフーのマニュアルとLINE Developersのドキュメントで確かめたもので、実際の画面は操作していません。文例は説明のための作例で、特定の店の文面ではありません。

LINEの自動返信は、4つの部品に分けて設計する

LINEの自動返信は、届く場面の違う4つの部品でできています。1つの長い文で全部を片づけようとすると、知りたい1行が埋もれて、結局スタッフに聞き直されます。

部品 動くとき 役目 管理画面での呼び名
あいさつ 友だち追加のとき 次に何を送ればよいかを伝える あいさつメッセージ
キーワード応答 決まった言葉が届いたとき よくある質問に1つの答えを返す 応答メッセージ(キーワード応答)
営業時間外の返信 人が返せない時間に届いたとき 受け付けたことと、返事の時期を伝える 応答時間、応答メッセージ(一律応答)
人へ引き継ぐ線 自動で答えない内容が届いたとき 自動の返事を止めて、スタッフに渡す 手動チャット

作る順番も、この表の上からです。ただし、文面を書き始める前に、一番下の「自動で答えないこと」だけは先に決めます。ここが決まると、キーワードの候補と、時間外の文面の書き方が決まります。

管理画面の応答メッセージで足りる形と、Webhookが要る形の線

線を引く基準は、返事を決めるのに「届いた文の全体」と「時刻」以外の情報が要るかどうかです。LINEヤフーのマニュアルでは、キーワード応答は設定したキーワードと完全に一致したメッセージに送られ、一律応答は内容に関わらずすべてのメッセージに返信されるとされています。この2つで表せる返事なら、管理画面で足ります。

やりたいこと 管理画面の応答メッセージ Webhookで作る
「体験」と送られたら案内を返す できる できる
営業時間外にひとこと返す できる できる
「体験の予約をしたいです」の中の言葉で答えを選ぶ 完全一致なので拾えない できる
「予約」と「キャンセル」が同じ文に入ったら、案内ではなく人に渡す 文の途中の言葉では分けられない できる
予約の有無や、会員かどうかで答えを変える 設定の項目にない できる(予約データとつなぐ)
人に渡した会話の自動返信を、決めた時間だけ止める 一時的な手動チャットで止める できる
自社の通知先に知らせる、返した答えごとに数える 設定の項目にない できる

表の中央の列は、応答メッセージの設定で確かめられる範囲の話です。管理画面にはこのほかに、登録したQ&Aから答えを選んで返すAIチャットボット(β)もあります。マニュアルではチャットProオプションの機能とされ、対象外の業種もあるので、使う前にマニュアルの利用条件を確かめます。AIに一次対応を任せる場合の範囲の決め方は、ジムの問い合わせをAIに任せる記事に書きました。

実際に分かれやすいのは業種です。この4業種での目安は次のとおりです。ここは経験則で、公式の基準ではありません。

業種 まず管理画面で足りる範囲 Webhookが欲しくなる場面
パーソナルジム 体験、料金ページ、持ち物、地図 体の相談と体験の申し込みが同じ文に混ざる。会員と体験前で答えを変える
整体院 予約ページ、メニュー、初回の流れ、地図 症状の話を自動で返さず確実に人へ渡す
結婚式場 フェアの予約、見学、地図 予約したフェアで答えを変える(来館前の自動応答の記事)
リフォーム会社 見積もりの依頼に要る項目の案内、施工写真、対応地域 写真が続けて届く。雨漏りのような急ぎを先に分ける

もう1つ、名前の混同に注意します。管理画面の「応答メッセージ」と、Messaging APIの応答メッセージ(reply)は、名前が同じでも別の機能です。この記事では、前者を「管理画面の応答メッセージ」と書きます。

自動返信は、あいさつから人への引き継ぎまで4つの順に作る

4つの部品は、どれも「相手が次に何をすればよいか」を1通で分かる形にします。業種ごとの文面は次の節にまとめ、ここでは決め方を書きます。

手順1. あいさつは、送ってほしい言葉を見せる

あいさつメッセージは、お礼より先に、次の行動を書きます。キーワード応答は完全一致で動くので、「体験」のように送ってほしい言葉をそのまま文中に出しておくと、当たる確率が上がります。マニュアルでは「友だちの表示名」を差し込めますが、表示名を含むメッセージは送信されないことがある相手も挙げられているので、なくても意味が通る文にします。

ブロック解除のときに同じメッセージを送らない設定もあります。応答設定であいさつメッセージを「オフ」にしていると、保存してあっても配信されません。

手順2. キーワード応答は、ボタンと組み合わせる

キーワード応答は、自由に打たれた文にはほとんど当たらない前提で設計します。入口はリッチメニューのボタンにして、決まった言葉を受け取る形が基本です。リッチメニューに置く項目は会社のコラムLINEリッチメニューの設計にまとめています。

1つの応答メッセージに設定できるキーワードは51個までです。「体験」「体験したい」「体験予約」「ご予約」のように、実際に届いた言い方を登録します。全角と半角、大文字と小文字をどう扱うかはマニュアルに書かれていないので、違う表記は別のキーワードとして入れておくほうが確実です。

応答メッセージを複数作ると、送られる順番が決まっています。完全に一致したキーワード応答が先で、一致しないときは、期間を指定した一律応答、時間を毎日で指定した一律応答、スケジュールのない一律応答の順に送られます。同じ条件の一律応答が複数あると、その中からランダムに1つ送られるので、一律応答は条件ごとに1つにします。

手順3. 営業時間外の返信は、受け付けたことと返事の時期を書く

営業時間外の作り方は2つあります。応答時間を使うなら、チャットと応答時間をオンにして、応答時間外の応答方法に応答メッセージを含む項目を選びます。スタッフが日中に手で返している店に向いています。チャットを使わないなら、一律応答にスケジュールを付けて、夜の時間だけ送る形もできます。

文面に入れるのは、受け付けたこと、いつ返事が届くか、急ぐときの連絡先の3つです。「営業時間外です」だけで終わると、相手は届いたのかどうかも分かりません。定休日の翌朝に返せない店は、「明日」ではなく「次の営業日」と書きます。応答時間には祝日の扱いの設定もあるので、祝日に休む店は合わせて決めます。

手順4. 人へ引き継ぐ線は、言葉と理由で先に書く

人に渡す線は、「難しそうなら」ではなく、どの言葉が来たら渡すかで書きます。この4業種で共通する線は次のとおりです。線の引き方は経験則です。

渡す理由 言葉の例 ジム・整体院 式場 リフォーム
体や健康の相談 痛、しびれ、持病、妊娠 渡す アレルギー、車いすなどを渡す ―
効果を聞かれた 治、効果、効き 渡す。自動で答えを出さない ― ―
契約・お金 退会、返金、見積、値引き 渡す 渡す 渡す(見積もりの依頼は、要る項目の案内を返してから)
変更・取り消し キャンセル、変更 渡す 渡す 渡す
急ぎ 雨漏り、水漏れ、遅れ、迷 ― 電話を先に出す 電話を先に出す
人を求めた 担当、電話 渡す 渡す 渡す

管理画面だけで運用する場合は、これらの内容が届いたらスタッフが手動チャットに切り替えます。マニュアルでは、手動チャットと応答メッセージを併用している場合や、応答時間外に応答メッセージで対応している際に、一時的に手動チャットへ切り替えられるとされています。一時的な手動チャットは1時間で自動的に終わり、「手動チャットを延長」で最長7日間まで延ばせます。切り替えを忘れると、スタッフが返している途中に一律応答が割り込むことがあります。

渡すときに何をそろえるかは、式場なら来館前の自動応答の記事の引き継ぎの節、ジムならAIの一次対応の記事が参考になります。

そのまま使える、業種ごとの返信の文例

ここからの文例は、〔 〕と時刻を店の情報に置き換えて使えます。価格、効果、受け入れの可否のような店ごとの判断は、文面に書かず、店が管理しているページへ案内するか、人に渡します。料金をLINEの文面に書くと、変更したときに直す場所が増えるためです。

業種1. パーソナルジム

場面 届く言葉 返す文
あいさつ 友だち追加 友だち追加ありがとうございます。体験のお申し込みは「体験」、料金は「料金」と送ってください。そのほかのご相談は、文章のまま送っていただければ担当者がお返事します。
キーワード 体験 体験のお申し込みはこちらです。空いている日時から選べます。〔体験予約のURL〕
キーワード 料金 料金はこちらのページに載せています。〔料金ページのURL〕
キーワード 持ち物 持ち物と設備はこちらです。〔案内ページのURL〕
営業時間外 すべて 受け付けました。営業時間外のため、次の営業日の10時以降にお返事します。お急ぎの場合は〔電話番号〕へお願いします。
人へ渡す 膝が痛い、持病がある 体の状態に関わるご相談は、担当のトレーナーからお返事します。このトークでお待ちください。
人へ渡す 退会、休会 担当者にお渡ししました。このあと、このトークでお返事します。

業種2. 整体院

場面 届く言葉 返す文
あいさつ 友だち追加 友だち追加ありがとうございます。ご予約は「予約」、メニューと所要時間は「メニュー」と送ってください。お体のご相談は、担当の者がこのトークでお返事します。
キーワード 予約 ご予約はこちらから、空いている日時を選べます。〔予約ページのURL〕
キーワード メニュー メニューと所要時間はこちらです。〔メニューページのURL〕
キーワード 初めて 初めての方の流れと持ち物はこちらです。〔初回案内のURL〕
営業時間外 すべて 受け付けました。次の受付時間の10時以降にお返事します。
人へ渡す 腰痛は治りますか、効果はありますか お体の状態によってお答えが変わるため、担当の者がこのトークでお返事します。
人へ渡す 予約を変更したい 担当の者が確認し、このトークでお返事します。施術中はお時間をいただくことがあります。

整体院でいちばん大事なのは、症状と効果の質問に自動で答えを出さないことです。「大丈夫です」も「お任せください」も、自動の返事には入れません。

業種3. 結婚式場

アクセス、駐車場、服装、所要時間の答えは、来館前の自動応答の記事で扱っています。ここでは、来館を決める前のお二人から届く言葉の文例を載せます。

場面 届く言葉 返す文
あいさつ 友だち追加 友だち追加ありがとうございます。ブライダルフェアと見学のご予約は「フェア」と送ってください。ご希望の日の空きは、候補の日を送っていただければ担当者がお返事します。
キーワード フェア ブライダルフェアと見学のご予約はこちらです。日程から選べます。〔予約ページのURL〕
文の中の言葉 来年の5月は空いていますか ご希望の日の空きは、担当者が確かめてお返事します。候補の日を2つほどお送りください。
営業時間外 すべて 受け付けました。次の営業日の10時以降に、担当者からお返事します。
人へ渡す 見積もり、特典、持ち込み ウェディングの担当者にお渡ししました。このトークでお返事します。
急ぎ 道に迷いました、遅れます ご連絡ありがとうございます。お急ぎの場合はお電話ください。〔電話番号〕

挙式日の空きは、自動で「空いています」と返しません。仮押さえと商談の状況で変わるためです。

業種4. リフォーム会社

リフォーム会社の問い合わせは、言葉より写真で届きます。最初の返事で、見積もりの相談に要る項目をそろえて聞くと、往復が減ります。

場面 届く言葉 返す文
あいさつ 友だち追加 友だち追加ありがとうございます。お見積もりの相談は「見積もり」と送ってください。施工の写真は「施工事例」で見られます。
キーワード 見積もり お見積もりの相談は、次の3つを送ってください。1. 工事の場所(例:浴室、外壁) 2. いまの写真を2〜3枚 3. 現地を見に伺える希望の日。担当者が確認してお返事します。
キーワード 施工事例 施工の写真はこちらです。〔施工事例のURL〕
写真が届いた 画像 写真を受け取りました。工事の場所と、気になっている点を文章でも送ってください。担当者が確認します。
営業時間外 すべて 受け付けました。次の営業日の9時以降にお返事します。
急ぎ 雨漏り、水漏れ お急ぎの内容は、お電話のほうが早くお受けできます。〔電話番号〕 営業時間外の場合は、次の営業日の9時以降にこのトークでお返事します。
人へ渡す 工事中、不具合、追加の費用 担当者にお渡ししました。このトークかお電話でお返事します。

写真が届いたときの返事は、管理画面のキーワード応答では作れません。マニュアルの説明では、一律応答は内容に関わらずすべてのメッセージに返信されるので、文面は「受け付けました」にとどめます。写真のときだけ別の文を返すのは、Webhookの範囲です。

返信が止まる、典型的な設定の誤り

自動返信が止まったときは、文面より先に設定を上から順に確かめます。マニュアルでは、応答メッセージの画面の上部「現在のステータス」欄で、いま送られている状態かを確認できるとされています。まずここを見ます。

症状 よくある原因 確かめる場所
どの文にも返らない チャットがオフで、応答メッセージもオフ 「設定」>「応答設定」
どの文にも返らない チャットがオンで、応答方法に応答メッセージを含む項目を選んでいない 「設定」>「応答設定」
日中だけ、または夜だけ返らない 応答時間内と応答時間外の片方にしか、応答メッセージを含む項目を選んでいない 応答時間と応答方法
作ったのに返らない 利用開始を押さず保存だけになっている。「利用」スイッチが切れている 応答メッセージのリスト
ある時期から返らない スケジュールの期間が過ぎている 応答メッセージのオプション設定
文によって返らない キーワードが完全一致していない 応答タイプとキーワード
違う文が返る、毎回変わる 同じ条件の一律応答が複数ある 応答メッセージのリスト
友だち追加で何も届かない あいさつメッセージがオフで、保存だけされている 「設定」>「応答設定」
Webhookに切り替えたら返らない 「Webhookの利用」が無効、またはWebhook URLの検証に失敗している LINE Developersコンソールの「Messaging API設定」、「エラーの統計情報」
2通届く Webhookの返事と、管理画面の応答メッセージが両方動いている 応答メッセージをオフにする
Webhookの返事だけ、ときどき返らない 応答トークンの使用済み、または期限切れ サーバーのログ(下のコードではreply 400。APIの返す文はInvalid reply token)

Webhookでは確かめる場所が増えます。LINE Developersコンソールの「エラーの統計情報」は初期設定で無効で、オンにした期間のエラーだけが集計されます。止まってから有効にしても、さかのぼって見られません。使い始めの日にオンにします。

あいさつメッセージと応答メッセージは、チャネルを作った時点で有効になっています。LINE Developersのドキュメントは併用もできるとしたうえで、どちらの返事か分からなくなることがあるので、初めてのボットでは両方をオフにするよう勧めています。

Webhookで作るときの、キーワード応答の部分

ここからは開発の担当者向けです。Webhookの受け口は署名検証の記事のsrc/index.jsをそのまま使い、processEventから次のautoReplyを呼ぶだけにしました。署名の検証、先に200を返すこと、webhookEventIdでの重複の除外は、そちらの記事のとおりです。Messaging APIのチャネルの作り方はLINE予約システムの記事の手順1にあります。

src/index.js(Webhookの記事から変えた部分)js
import { autoReply } from "./auto-reply.js";

async function processEvent(env, event) {
  await autoReply(env, event);
}

判定は、送信とデータベースから切り離す

1通の文にどう返すかを決める処理は、外の世界に触らない関数にしました。こうしておくと、文面を変えるたびに言葉の判定だけを手元で試せます。

src/decide.jsjs
const JST_MS = 9 * 60 * 60 * 1000;

// 全角・半角、大文字・小文字、空白、かぎかっこの違いをなくす
export const normalize = (s) => s.normalize("NFKC").toLowerCase().replace(/[\s「」『』"']/g, "");

// 営業時間の中か。hours は日本時間の分で持つ(10:00 なら 600)
export function isOpen(ms, hours) {
  const d = new Date(ms + JST_MS);
  if (hours.closedDays.includes(d.getUTCDay())) return false;
  const min = d.getUTCHours() * 60 + d.getUTCMinutes();
  return min >= hours.open && min < hours.close;
}

// 1通の文に、どう返すかを決める。ここでは送信もデータベースも触らない
export function decide(shop, text, ms) {
  const t = normalize(text);
  const hit = (words) => words.some((w) => t.includes(normalize(w)));
  const open = isOpen(ms, shop.hours);

  // 人に渡す語を、質問の語より先に見る
  const staff = shop.handoff.find((r) => hit(r.words));
  if (staff) {
    return { kind: "handoff", rule: staff.key, reply: staff.reply ?? (open ? shop.staffOpen : shop.staffClosed) };
  }
  const faq = shop.faq.find((r) => hit(r.words));
  if (faq) return { kind: "faq", rule: faq.key, reply: faq.reply };
  return { kind: "unknown", rule: "unmatched", reply: open ? shop.unknownOpen : shop.unknownClosed };
}

順番に意味があります。「予約をキャンセルしたいです」には「予約」も入っているので、質問の言葉を先に見ると予約の案内を返してしまいます。人に渡す言葉を先に見れば、この取り違えは起きません。

店ごとの言葉と文面は、コードから分けて持ちます。ジムの例です。

src/shops.js(ジムの分)js
const week = { open: 10 * 60, close: 21 * 60, closedDays: [] };

export const gym = {
  hours: { ...week, closedDays: [2] }, // 2 は火曜
  staffOpen: "担当者にお渡ししました。このあと、このトークでお返事します。",
  staffClosed: "受け付けました。営業時間外のため、次の営業日の10時以降にお返事します。",
  unknownOpen: "内容を確認して、担当者からお返事します。",
  unknownClosed: "受け付けました。次の営業日の10時以降に、担当者からお返事します。",
  handoff: [
    { key: "health", words: ["痛", "しびれ", "治", "効果", "効き", "ケガ", "怪我", "持病", "妊娠", "通院", "薬"] },
    { key: "contract", words: ["退会", "休会", "解約", "返金", "クレーム"] },
    { key: "change", words: ["キャンセル", "変更", "遅れ"] },
    { key: "person", words: ["スタッフ", "担当", "人と話", "電話"] },
  ],
  faq: [
    { key: "trial", words: ["体験", "予約"], reply: "体験のお申し込みはこちらです。空いている日時から選べます。\n〔体験予約のURL〕" },
    { key: "price", words: ["料金", "値段", "いくら", "月謝"], reply: "料金はこちらのページに載せています。\n〔料金ページのURL〕" },
    { key: "items", words: ["持ち物", "服装", "手ぶら", "シャワー"], reply: "持ち物と設備はこちらです。\n〔案内ページのURL〕" },
    { key: "access", words: ["場所", "行き方", "アクセス", "駐車場", "最寄り"], reply: "地図と駐車場の案内はこちらです。\n〔地図のURL〕" },
  ],
};

人に渡したら止め、続けて届いても1回だけ返す

送信と記録は別のファイルにします。表は、Webhookの記事のline_eventsに次の2つを足しました。お客様の文はデータベースに入れません。ジムや整体院の文には体の話が入るためです。

schema.sql(足した分)sql
-- 人に渡した会話。いつまで自動の返事を止めるかを持つ
CREATE TABLE conversations (
  user_id       TEXT PRIMARY KEY,
  handoff_until INTEGER NOT NULL DEFAULT 0, -- この時刻まで自動で返さない
  unknown_until INTEGER NOT NULL DEFAULT 0  -- この時刻まで「確認します」を重ねて返さない
);

-- 何に返したかの記録。お客様の文は持たない
CREATE TABLE reply_log (
  webhook_event_id TEXT PRIMARY KEY,
  kind             TEXT NOT NULL, -- faq / handoff / unknown / silent
  rule             TEXT NOT NULL, -- どの決まりに当たったか
  event_timestamp  INTEGER NOT NULL
);
src/auto-reply.jsjs
import { decide } from "./decide.js";
import { gym as shop } from "./shops.js";

const HOUR = 60 * 60 * 1000;
const HANDOFF_MS = 24 * HOUR; // 人に渡したら、24時間は自動で返さない
const UNKNOWN_MS = 30 * 60 * 1000; // 分からない文の返事は30分に1回

// Webhook の記事の processEvent から呼ぶ
export async function autoReply(env, event) {
  // 1対1のトークだけ返す。standby のイベントには replyToken がない
  if (event.source?.type !== "user" || event.type !== "message" || !event.replyToken) return;

  const userId = event.source.userId;
  const ms = event.timestamp; // 受けた時刻ではなく、送られた時刻で判定する
  const c = await env.DB.prepare("SELECT handoff_until FROM conversations WHERE user_id = ?1").bind(userId).first();
  if ((c?.handoff_until ?? 0) > ms) return log(env, event, "silent", "handoff");

  const d = choose(event.message, ms);
  if (!d) return log(env, event, "silent", event.message.type);

  // 分からない文と写真への返事は、続けて届いても1回にする
  if (d.kind === "unknown" && !(await claimUnknown(env, userId, ms))) return log(env, event, "silent", d.rule);
  if (d.kind === "handoff") await holdForStaff(env, userId, ms);

  let failure = null;
  try {
    await reply(env, event.replyToken, d.reply);
  } catch (err) {
    failure = err;
    if (d.kind === "unknown") await releaseUnknown(env, userId); // 返せなかったので、次の文に返せるよう戻す
  }
  await log(env, event, d.kind, d.rule);
  if (d.kind === "handoff") await notifyStaff(env, d.rule); // 返事に失敗しても、店には必ず知らせる
  if (failure) throw failure;
}

function choose(message, ms) {
  if (message.type === "text") return decide(shop, message.text, ms);
  if (message.type === "image" && shop.imageReply) return { kind: "unknown", rule: "image", reply: shop.imageReply };
  return null; // スタンプなどには返さない
}

function holdForStaff(env, userId, ms) {
  return env.DB.prepare(
    `INSERT INTO conversations (user_id, handoff_until) VALUES (?1, ?2)
     ON CONFLICT (user_id) DO UPDATE SET handoff_until = excluded.handoff_until`,
  )
    .bind(userId, ms + HANDOFF_MS)
    .run();
}

// 確かめて書く、を1文で行う。同時に届いても、取れるのは1件だけ
async function claimUnknown(env, userId, ms) {
  const r = await env.DB.prepare(
    `INSERT INTO conversations (user_id, unknown_until) VALUES (?1, ?2)
     ON CONFLICT (user_id) DO UPDATE SET unknown_until = excluded.unknown_until
     WHERE conversations.unknown_until <= ?3`,
  )
    .bind(userId, ms + UNKNOWN_MS, ms)
    .run();
  return r.meta.changes === 1;
}

function releaseUnknown(env, userId) {
  return env.DB.prepare("UPDATE conversations SET unknown_until = 0 WHERE user_id = ?1").bind(userId).run();
}

function log(env, event, kind, rule) {
  return env.DB.prepare(
    "INSERT INTO reply_log (webhook_event_id, kind, rule, event_timestamp) VALUES (?1, ?2, ?3, ?4)",
  )
    .bind(event.webhookEventId, kind, rule, event.timestamp)
    .run();
}

async function reply(env, replyToken, text) {
  const res = await fetch(`${env.LINE_API_BASE}/v2/bot/message/reply`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.LINE_CHANNEL_ACCESS_TOKEN}` },
    body: JSON.stringify({ replyToken, messages: [{ type: "text", text }] }),
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) throw new Error(`reply ${res.status}`);
}

// 店の通知先には理由だけを送る。お客様の文はチャットの画面で読む
async function notifyStaff(env, reason) {
  const res = await fetch(env.STAFF_NOTIFY_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: `LINEの問い合わせを人に渡しました(理由:${reason})。チャットの画面を確認してください。` }),
    signal: AbortSignal.timeout(5_000),
  });
  if (!res.ok) throw new Error(`notify ${res.status}`);
}

要点は5つです。

  • 判定の時刻はevent.timestamp。 リファレンスでは、再送されたWebhookでも、再送の時刻ではなくイベントの発生時刻を表すとされています
  • 止める時間は24時間、分からない文の返事は30分に1回。 どちらもこの記事で仮に決めた値で、公式の基準ではありません。店の返事の速さに合わせて変えます
  • 店の通知には理由だけを書く。 原文はチャットの画面で読みます。通知先のチャットツールに体の話が流れないようにするためです
  • 応答トークンは待たせない。 リファレンスでは、一度だけ使え、Webhookを受信してから1分以内に使う必要があり、時間制限に依存した実装をしないようにとされています
  • 値はコードに書かない。 LINE_API_BASEは本番ではhttps://api.line.meにします。LINE_CHANNEL_ACCESS_TOKENとSTAFF_NOTIFY_URLは、チャネルシークレットと同じくwrangler secret putで登録し、gitに入れません

試した結果

署名付きのイベントをwrangler devのWorkerに送り、真似るサーバーが受けた内容を確かめました。判定の関数は、4業種の39の文と、正規化、時間外の文面、営業時間の境目を入れた42項目で確かめています。結合の試験はジムの19項目を5回とリフォーム会社の3項目を2回流し、最後はすべて通りました。

試したこと 結果
「体験の予約をしたいです」 体験の案内を1通返した
同じwebhookEventIdの再送 2通目を送らなかった
「膝が痛いけど体験できますか」 人に渡す返事を1通と、店への通知を1回。通知に本文は入らなかった
渡した1分後に「駐車場はありますか」 何も返さなかった
渡した25時間後に同じ文 自動の返事に戻った
23時30分に「退会したい」 営業時間外の文を返した
分からない文を3通続けて(1リクエスト) 1通だけ返した
分からない文を8通同時に(別々のリクエスト) 1通だけ返した
分からない文のあとに「駐車場はありますか」 答えを返した
グループのトーク、スタンプ、standbyのイベント どれも返さなかった
写真を3枚続けて(リフォーム会社) 1通だけ返した
23時30分に「天井から雨漏りしています」(リフォーム会社) 電話番号を先に出す文を返し、店に通知した

最初の版では2項目が通りませんでした。実際の出力です。

NG   返せなかった直後の文には返す replies=0
NG   店の通知先が500でも、お客様には返す replies=0 staff=1

1つ目は、応答トークンが切れて返せなかったのに「30分は返さない」印だけが残っていました。2つ目は、店への通知を返事より先に送っていたので、通知先が止まるとお客様への返事まで止まっていました。上のコードは直した後のものです。返事を先に送り、失敗したら印を戻し、返事に失敗しても店には知らせる順にしています。

もう1点、同時に届く場合も直しました。最初の版は「読んでから書く」の2段階で、4通同時の試験は1通で通りました。ただ、1回通っても守られている証拠にはならないので、確かめと書き込みを1文にしたclaimUnknownに変えています。

言葉の一致で外れる文

文の中の言葉で選ぶ形は、完全一致より拾える文が増えますが、外れもあります。試した中で外れたのは次の文です。

文 判定 理由
ちゅうしゃじょうありますか 分からない文 ひらがなの語を登録していない
パーキングある? 分からない文 言い換えを登録していない
タイケン 分からない文 半角カナはカタカナに直るが、漢字にはならない
痛くないです。体験お願いします 人に渡す 否定は読めない
うちの地域は来てもらえますか 現地確認の返事 「来て」が「地域」より先に当たった
写真を送ります 施工事例の返事 「写真」が当たり、送るのか見たいのかは分からない
腰が悪いけど体験できますか 体験の案内 「悪い」を登録していないので、体の相談に気づけない

1つ目から3つ目は「確認します」が返って人が読むので、害は小さく、語を足せば直ります。4つ目は人に渡る側に外れるので、そのままにします。困るのは5つ目から7つ目のように、違う答えを返す外れです。これを減らしたくても、言葉の一致には限界があります。AIで意図を読む場合の範囲はジムの記事に書きましたが、この形でも、返す文は店が書いたものだけに保ちます。

このコードは祝日と臨時休業を知りません。店の休みの日を表に持つか、応答時間と合わせた運用が要ります。スタッフがチャットで返したことも、このコードには分かりません。だから、自動に戻す時刻を時間で決めています。

店に通知するのは人に渡す文だけで、分からない文は通知しません。「担当者からお返事します」と返す以上、応答設定でチャットをオンにして、店が画面を見る時間を決めておきます。

やってはいけないこと

自動返信でよくある失敗を、4業種に共通するものに絞って挙げます。どれも、返事が止まるより相手の信頼を損ねやすい失敗です。

  • 一律応答で全部を受ける。 文の内容に関わらず同じ文が返るので、「退会したい」にも案内が返ります。一律応答に書くのは「受け付けました」までにします
  • 自動の返事で、症状や効果、空き日を言い切る。 店の判断が要る内容は、文面に入れず人に渡します
  • 料金の金額を文面に直接書く。 改定のたびに直す場所が増え、古い文が残ります。料金は1つのページに案内します
  • お客様の文をそのまま店の通知先や記録に流す。 体の話や連絡先が入ります。見られる人を絞った場所に置きます
  • Webhookへ切り替えたのに応答メッセージをオンのままにする。 返事が重なり、どちらの返事か分からなくなります
  • 応答トークンを取っておいて後で使う。 一度だけで時間の制限があります。後から送る連絡はプッシュメッセージにします。送った相手の人数で通数に数えられるので、送る相手と回数を先に決めます

公開前に確かめること

自動返信は、公開の前に実機で次の項目を通します。設定を変えた日にも、同じ表で確かめ直します。

  • 自動で答えないことを、言葉と理由の表にした
  • 友だち追加から、案内どおりの言葉を送って答えが返る
  • 応答時間内と応答時間外の両方で、意図した文が返る
  • 定休日、祝日、夜中に送って、「次の営業日」の案内が正しい
  • 応答メッセージの「現在のステータス」が想定どおり
  • スケジュールを付けた応答メッセージの期間が切れていない
  • 実際に届いた文を20通ほど試し、違う答えが返らない
  • 人に渡す言葉で、自動の返事が止まり、スタッフが気づける
  • 急ぎの文で電話番号が最初に出る
  • Webhookの場合、「検証」が成功し、「エラーの統計情報」をオンにした
  • Webhookの場合、管理画面の応答メッセージとあいさつの扱いを決めた
  • iPhoneとAndroidの実機で、ボタンを押して返事が届く
  • 文面の直す担当を決めた

動かして確かめた範囲

2026年11月1日に、手元のMacで確かめました。

項目 バージョン・内容
OS macOS 26
Node.js 24.22.1(node --test、試験のスクリプト)
wrangler 4.156.0(wrangler dev、手元のD1)
compatibility_date 2026-10-01
LINEのAPI 手元の真似るサーバー。本物のLINEには送っていない

確かめたこと:

  • decideの判定を4業種の39の文で確かめ、正規化、時間外の文面、営業時間の境目を含む42項目が通りました
  • 署名付きのイベントを手元のWorkerに送る結合の試験で、ジムの19項目(5回)とリフォーム会社の3項目(2回)が通りました。リフォーム会社の分は、auto-reply.jsの読み込みをreformに変えて流しています
  • 真似るサーバーでは、トークンのヘッダー、メッセージの件数、文字数を確かめて応答を返しています

確かめていないこと:

  • 本物のLINEとのやり取りのすべて。チャネルの作成、コンソールの「検証」、実際のWebhookの形、応答の到着、スマホでの見え方は見ていません。真似るサーバーはドキュメントを読んで書いたもので、本物と同じ動きとは限りません
  • 管理画面の応答メッセージ、あいさつメッセージ、応答時間の実際の動き。設定の説明はマニュアルの記述によるもので、手元のアカウントで試していません
  • 管理画面の応答メッセージとWebhookを両方オンにしたときの、実際の返事の順番
  • 手動チャットへの切り替えと、Webhookの返事の関係。このコードは手動チャットの状態を見ません
  • Cloudflareへのデプロイ、本番のD1。同時に届く試験は手元のD1での結果です
  • 店の通知先。チャットツールやメールの代わりに、真似るサーバーへPOSTしただけです
  • 試験のコードは長いので載せていません。署名付きのイベントの作り方はWebhookの記事のtest.mjsと同じです

自社でやる場合と、任せる場合

管理画面で足りる範囲なら、公式LINEの担当者が自社で組めます。必要なのは、実際に届いた問い合わせを集める作業、文面を書く時間、当たらなかった文を週に一度読んでキーワードを足す手間です。Webhookで作る場合は、Messaging APIを読んでコードを直せる開発の担当が1人要り、失敗の見張り、トークンの管理、言葉の表の更新が続きます。自動で答えない線を決める仕事は、どの進め方でも店でしかできません。

任せる場合、株式会社bundlyzeでは、Web・システムのご相談は内容を問わずお受けしていて、公式LINEと予約・顧客管理をつなぐ仕組みも開発しています。入口はシステム開発のページです。

出典(一次情報)

確認日はどれも2026年11月1日です。LINEの仕様と画面は変わることがあるので、設定する時点で確かめてください。

よくある質問

LINEの自動返信は、管理画面の設定だけで作れますか?

あいさつ、決まった言葉へのキーワード応答、営業時間外の返信は、LINE Official Account Managerの設定で作れます。文の中の言葉で答えを選ぶ、相手の予約によって答えを変える、人に渡した会話の自動返信を止めて記録する、といったことをしたくなったら、Messaging APIとWebhookの出番です。

キーワード応答が反応しないのはなぜですか?

キーワード応答は、設定したキーワードと完全に一致したメッセージにだけ送られます。キーワードが「体験」なら、「体験の予約をしたいです」という文には送られません。入口をボタンにして決まった言葉を送ってもらうか、当てはまらない文に返す一律応答を用意します。文の言葉は合っているのに反応しないときは、応答設定、応答メッセージの利用中の状態、スケジュールの順に確かめます。

営業時間外だけ自動で返信するには、どう設定しますか?

「設定」>「応答設定」でチャットと応答時間をオンにし、応答時間外の応答方法に応答メッセージを含む項目を選びます。応答時間内はスタッフがチャットで返し、時間外だけ自動で返す形です。一律応答にスケジュールを付けて、時間を指定する方法もあります。文面には、受け付けたことと、いつ返事が届くかを書きます。

応答メッセージとWebhookは、同時に使えますか?

併用はできますが、LINE Developersのドキュメントは、どちらの返事か開発者が混乱することがあるとして、Messaging APIで初めてボットを作るときは、あいさつメッセージと応答メッセージをオフにするよう勧めています。Webhookで返すなら、応答メッセージはオフにします。WebhookとチャットはLINE公式アカウントで併用できるので、スタッフはチャットの画面で返せます。

整体院の自動返信で、症状の質問にはどう答えればよいですか?

自動で答えを出さず、症状の相談として担当者に渡します。「痛」「しびれ」「治」「効果」のような言葉を、質問の言葉より先に確かめ、受け取ったことと返事の目安だけを返します。「腰痛の施術の予約」のように予約の言葉が一緒に入っていても人に渡る側に倒すのが、この設計の考え方です。

この記事の書き方と確認の方針