LINE Messaging APIの始め方 予約受付と詰まる所
公式LINEに
この
この
この
載せた
Messaging APIは、Webhookで受けてAPIで返す仕組み
Messaging APIの
予約受付に
| 相手の操作 | サーバーに |
サーバーが |
送り方 |
|---|---|---|---|
| 友だち追加 | follow |
空いている |
応答 |
| 「予約」と |
message |
空いている |
応答 |
| 時間の |
postback |
枠を |
応答 |
| 操作なし |
なし | 予約の |
プッシュ |
決まった
手順1. チャネルは公式アカウント側から作り、プロバイダーを先に決める
Messaging APIの
いちばん
- チャネルと
プロバイダーを、 別々の 個人や 企業が 管理している - 関係の
ない サービスの チャネルを、 同じ プロバイダーの 下に 作る - 運用ツールを
提供する 企業の プロバイダーの 下に チャネルを 作る
ユーザーIDは
もう
手順2. チャネルアクセストークンは、4種類から選ぶ
サーバーから
トークンは
| 種類 | 有効期間 | 発行できる |
発行の仕方 | 向いている |
|---|---|---|---|---|
| v2.1 |
最大30日で |
30件。 |
JWTを |
LINEが |
| ステートレス | 15分 | 制限なし | APIで |
使うたびに |
| 短期 | 30日 | 30件。 |
APIで |
期限の |
| 長期 | 無期限 | 1件 | コンソールの |
まず |
LINE Developersの
長期の
v2.1の
値は
LINE_CHANNEL_SECRET=
LINE_CHANNEL_ACCESS_TOKEN=手順3. Webhookは、署名を確かめてから先に200を返す
受け口の
// server.mjs:Webhook を受けて、ボタンで予約を受ける最小のサーバー
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const API = process.env.LINE_API_BASE ?? "https://api.line.me";
const SECRET = process.env.LINE_CHANNEL_SECRET;
const TOKEN = process.env.LINE_CHANNEL_ACCESS_TOKEN;
if (!SECRET || !TOKEN) throw new Error("LINE_CHANNEL_SECRET と LINE_CHANNEL_ACCESS_TOKEN を入れてください");
// 題材の枠と処理済みのID。動きを見るだけなのでメモリに置く
const slots = [
{ id: "1", label: "11/05 10:00", left: 1 },
{ id: "2", label: "11/05 11:00", left: 3 },
];
const seen = new Set();
// 受け取ったバイト列のまま、署名を確かめる
function verify(rawBody, signature) {
const expected = createHmac("sha256", SECRET).update(rawBody).digest();
const given = Buffer.from(signature ?? "", "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
}
async function reply(event, messages) {
const res = await fetch(`${API}/v2/bot/message/reply`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}` },
body: JSON.stringify({ replyToken: event.replyToken, messages }),
signal: AbortSignal.timeout(10_000),
});
if (res.ok) return;
// 問い合わせに使うので、ステータス、リクエストID、本文を残す
throw new Error(`reply ${res.status} ${res.headers.get("x-line-request-id")} ${await res.text()}`);
}
const slotButtons = () =>
slots
.filter((s) => s.left > 0)
.slice(0, 13) // クイックリプライは13個まで
.map((s) => ({
type: "action",
action: { type: "postback", label: s.label, data: `action=book&slot=${s.id}`, displayText: s.label },
}));
function chooseSlot(event) {
const items = slotButtons();
if (items.length === 0) return reply(event, [{ type: "text", text: "いま予約できる枠がありません。" }]);
return reply(event, [{ type: "text", text: "時間を選んでください。", quickReply: { items } }]);
}
async function handleEvent(event) {
if (seen.has(event.webhookEventId)) return; // 同じイベントは1回だけ処理する
seen.add(event.webhookEventId);
if (!event.replyToken) return; // 友だち解除や standby には応答トークンがない
if (event.type === "follow") return chooseSlot(event);
if (event.type === "message" && event.message.type === "text" && event.message.text.includes("予約")) {
return chooseSlot(event);
}
if (event.type !== "postback") return;
const p = new URLSearchParams(event.postback.data);
if (p.get("action") === "dates") return chooseSlot(event);
if (p.get("action") === "book") {
const slot = slots.find((s) => s.id === p.get("slot"));
if (!slot || slot.left <= 0) return chooseSlot(event); // 見せた後に埋まった枠
slot.left -= 1;
return reply(event, [{ type: "text", text: `${slot.label} のご予約を受け付けました。` }]);
}
}
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhook") return res.writeHead(404).end();
const chunks = [];
try {
for await (const chunk of req) chunks.push(chunk);
} catch {
return res.destroy(); // 本文の途中で切れた接続。拾わないとサーバーごと落ちる
}
const rawBody = Buffer.concat(chunks);
if (!verify(rawBody, req.headers["x-line-signature"])) return res.writeHead(401).end();
res.writeHead(200).end(); // 署名が合ったら先に200を返し、処理はその後
const { events = [] } = JSON.parse(rawBody); // 空の配列なら何もしない
for (const event of events) {
await handleEvent(event).catch((err) => console.error(event.webhookEventId, err.message));
}
}).listen(process.env.PORT ?? 3000);動かすコマンドは--env-fileは.envの
node --env-file=.env server.mjsコードの
- 本文は
バイト列の 署名はまま 検証する。 チャネルシークレットを 鍵に した HMAC-SHA256です。 LINEの ドキュメントは、 検証の 前の JSONの 読み込み、 整形、 エスケープの 解釈、 UTF-8以外での 処理を 禁じています。 Expressなどの 本文を 自動で 読み込む 道具を 使う 場合は、 読み込む前の 本文を 取って おきます - 2秒より
前に ドキュメントの返す。 「エラーの 統計情報」の 説明では、 2秒以内に レスポンスが 返らないと request_timeoutになると されています。 予約の 処理を 終えてから 返すのではなく、 署名が 合ったら 先に 200を 返します - 空の
eventsにも200を 疎通の返す。 確認の ために、 イベントの ない リクエストが 届く ことがあります - ヘッダー名の
大文字と ヘッダー名の小文字に 頼らない。 表記は 予告なく 変わる ことが あると されています。 Node.jsの req.headersは小文字に そろえてくれるので、 x-line-signatureで読めます
Workersではctx.waitUntil()が
サーバーを
- 「Messaging API設定」
タブの 「Webhook URL」に、 https://(公開したドメイン)/webhookを入れる - 「検証」を
押して 「成功」と 出る ことを 確かめる - 「Webhookの
利用」を 有効に する - 同じ
タブの QRコードで、 公式アカウントを 友だち追加する - LINE Official Account Managerで、
あいさつ メッセージと 応答メッセージの 扱いを 決める
Webhook URLは
チャネルを
会社サイトの
手順4. 応答とプッシュは、きっかけで使い分ける
Messaging APIの
| 応答メッセージ | プッシュメッセージ | |
|---|---|---|
| きっかけ | 友だち追加、 |
いつでも |
| 宛先の指定 | 応答トークンreplyToken) |
ユーザーIDto) |
| 料金プランの |
数えられない | 送信の |
| 時間の制限 | 応答トークンは |
なし |
| 送り直し | X-Line-Retry-Keyは使えない |
X-Line-Retry-Keyで |
| 予約受付での |
枠の |
前日の |
1回で
応答トークンには
返事に
プッシュは、
な
// push.mjs:自分のユーザーIDに1通送る。node --env-file=.env push.mjs <ユーザーID> <文>
import { randomUUID } from "node:crypto";
const API = process.env.LINE_API_BASE ?? "https://api.line.me";
const [to, text] = process.argv.slice(2);
const res = await fetch(`${API}/v2/bot/message/push`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
"X-Line-Retry-Key": process.env.RETRY_KEY ?? randomUUID(), // 送り直すときは同じ値を使う
},
body: JSON.stringify({ to, messages: [{ type: "text", text }] }),
});
console.log(res.status, res.headers.get("x-line-request-id"), await res.text());X-Line-Retry-Keyには、
メッセージ通数は、送った相手の人数で数える
メッセージ通数は、
| 送り方 | 通数 | 予約受付での |
|---|---|---|
| 応答 | 数えられない | 枠の |
| プッシュ | 数えられる | 前日の |
| マルチキャスト | 数えられる | 指定した |
| ナローキャスト | 数えられる | 属性や |
| ブロードキャスト | 数えられる | 友だち全員への |
ブロックされている
見積もる
月の通数 = 月の予約の件数 × 予約1件あたりのプッシュの回数 + 友だちの数 × 月の配信の回数たとえば
上限を
// quota.mjs:今月の上限の目安と、使った通数を見る
const API = process.env.LINE_API_BASE ?? "https://api.line.me";
const get = async (path) => {
const res = await fetch(`${API}${path}`, {
headers: { Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}` },
});
return [res.status, await res.json()];
};
console.log("上限の目安", ...(await get("/v2/bot/message/quota")));
console.log("使った通数", ...(await get("/v2/bot/message/quota/consumption")));上限のtypeがlimitedのときだけvalueが返り、noneなら
リッチメニューは、ポストバックが要るならAPIで作る
リッチメニューは、
| LINE Official Account Manager | Messaging API | |
|---|---|---|
| 作り方 | 画面で |
定義を |
| 向いている |
予約ページなどへの |
ボタンを |
| ポストバック | 利点に |
使える |
| 表示期間の |
できる | 利点には |
予約ページがserver.mjsのaction=datesは、dataと
作る
- 1つの
メニューには 管理画面で1つの 道具だけ。 作った メニューは APIで 扱えず、 APIで 作った メニューは 管理画面で 扱えません - パソコン版の
LINEには リッチメニューは出ない。 デスクトップ版 (macOS、 Windows)では 表示されません。 文字で 「予約」と 届いた 場合にも 同じ 案内を 返しておくと、 メニューが 見えない 人の 受け口に なります - APIの
メニューが 表示の優先される。 順は、 ユーザー単位、 APIの 既定、 管理画面の 既定です。 管理画面で 直したのに 変わらない ときは、 APIの 既定が 残っていないかを 見ます - 既定の
メニューは 既定のすぐには 変わらない。 メニューは、 トーク画面に 入り直した ときに 反映されます。 APIの 既定は、 反映まで 1分ほど かかる ことがあります
予約の
よくあるエラーは、ステータスとmessageで切り分ける
Messaging APIのmessageでserver.mjsは、X-Line-Request-Id、
| 見えるもの | 意味 | 確かめる |
|---|---|---|
| APIが |
有効な |
トークンのBearer の付け忘れ |
400 Invalid reply token |
応答トークンが |
同じ |
400 The request body has X error(s) |
送った |
detailsのpropertyが |
| 403 | 権限がない | 契約中の |
404 Not found |
プロフィールを |
友だち追加の |
| 409 | 同じリトライキーが |
重複を |
429 You have reached your monthly limit. |
月の |
当月の |
429 The API rate limit has been exceeded. Try again later. |
レート制限を |
同じ |
Failed to send messages |
送信に |
ユーザーIDの |
自社の
| 見えるもの | 意味 | 確かめる |
|---|---|---|
| 自社の |
シークレットの |
シークレットの |
could_not_connect |
サーバーへ |
HTTPS、 |
request_timeout |
2秒以内に |
先に |
error_status_code |
200番台以外を |
サーバーの |
| 同じ |
管理画面の |
LINE Official Account Managerの |
シークレットを
could_not_connectなどの
最小のコードの次に足すもの
ここまでの
| 足したい |
変えること | 詳しい記事 |
|---|---|---|
| 枠と |
データベースに |
LINE予約システムの |
| 再送と |
webhookEventIdを |
Webhookの |
| 前日に |
定期実行で |
前日リマインド |
| 自由な |
文の |
LINE |
| 予約の |
LINEの |
公式LINE連携の |
業種で
名前や
会社サイトの
やってはいけないこと
- トークンや
シークレットを 例のコードや gitに 入れる。 値でも、 本物と 同じ形の 文字列を 書かないようにします。 漏れた 疑いが あれば、 LINEの ドキュメントの とおり取り消して 発行し直します - 検証の
前に 中身が本文を JSONに する。 同じでも 署名が 合わなくなります - Webhookを
IPアドレスで LINEの絞る。 ガイドラインで 禁じられています。 LINEの プラットフォームの IPアドレスは 開示されておらず、 守りは 署名の 検証で 行います - 本物の
LINEで ガイドラインは、負荷試験を する。 大量の リクエスト、 プラットフォームを 経由した 負荷テスト、 同じ ユーザーへの 大量送信を 禁じています。 手元の 試験は、 この 記事のように 真似る サーバーで 行います - 応答トークンを
取って 後からおいて 後で 使う。 送る 連絡は プッシュにします - ボタンの
dataをそのまま 見せた信じる。 後に 枠が 埋まる ことが あり、 トークに 残る ボタンなら 古い ものも 押せます。 枠の 空きと 本人を、 押された 時点で 確かめ直します
公開前に確かめること
- プロバイダーが、
今後LINEログインを 足す ものと 同じに なっている - 本物の
チャネルで、 コンソールの 「検証」が 成功する - 運用で
使う トークンの 種類を 決め、 発行し直す 手順を 書いた - シークレットと
トークンが gitの 履歴に 残っていない - 管理画面の
あいさつ メッセージと 応答メッセージの 扱いを 決めた -
「エラーの 統計情報」を オンに した - 友だち追加から
予約の 受け付けまでを、 iPhoneと Androidの 実機で 通した - リッチメニューの
ボタンが、 意図した dataで届く - パソコン版の
LINEから 「予約」と 送っても 案内が 返る - 月の
通数を 見積もり、 当月の 状況を 見る 人を 決めた - 失敗の
ログに、 リクエストIDが 残っている - サーバーが
止まった ときの 連絡先を 決めた
動かして確かめた範囲
2026年11月1日に、
| 項目 | バージョン・内容 |
|---|---|
| OS | macOS 26 |
| Node.js | 24.22.1 |
| Webhook | 手元でserver.mjsへ送った |
| LINEの |
手元の |
server.mjsは90行、push.mjsは16行、quota.mjsは
| 試したこと | 結果 |
|---|---|
| 日本語、 |
200。 |
| 違う鍵、 |
どれも |
空のevents |
200で、 |
友だち追加、action=dates |
枠の |
| 残り1の |
1回目は |
同じwebhookEventIdの再送 |
2回目は |
| 本文の |
サーバーは |
友だち解除、standby、スタンプ |
どれも |
| 使用済みの |
ログにreply 400と |
| APIが |
Webhookは |
push.mjs、 |
200、409 |
間push.mjs |
401 |
quota.mjs |
2つの |
エスケープの\nを\u000aに
確かめていない
- 本物の
LINEとの やり取りの すべて。 チャネルの 作成、 トークンの 発行、 コンソールの 「検証」、 実際の Webhookの 形、 応答と プッシュの 到着、 ボタンの 見え方は 見ていません。 真似る サーバーは ドキュメントを 読んで 書いた もので、 本物と 同じ 動きとは 限りません - 上の
エラーの 表は、 ドキュメントの ステータスコードと エラーメッセージから 書いた ものです。 動かして 試したのは、 真 似る サーバーが 返す400と 401と 409だけです - 公開の
環境への デプロイと、 HTTPSの 設定。 2秒の 制限を 実際の ネットワークで 測っていません - v2.1の
トークンの 発行と、 リッチメニューの 作成。 リッチメニューの スクリプトは予約システムの 記事 のもので、 この 記事では 動かしていません - ローディングの
アニメーションの API - 月の
通数の 上限を 超えた ときの 実際の 動き。 上限を 超えた 後に 応答メッセージが 送れるかは、 確かめた ドキュメントの 範囲では 読み取れませんでした - 大きな
本文を 送りつけられた ときの 備え。 server.mjsは本文の 大きさを 制限していないので、 本番では 上限を 付けます
自社でやる場合と、任せる場合
最初の
任せる
出典(一次情報)
確認日は
- LINE Developers
「Messaging APIの :ボットサーバーと概要」 LINEプラットフォームの やり取り - LINE Developers
「Messaging APIを :2024年9月4日の始めよう」 直接作成の 廃止、 プロバイダーを 変更できない こと、 Admin権限、 注意が 要る 場合 - LINE Developers
「ボットを :トークンの作成する」 勧め、 Webhook URLが 1つなこと、 HTTPSと 証明書、 検証、 IPアドレスでの 呼び出し元の 制限、 あいさつ メッセージと 応答メッセージ - LINE Developers
「チャネルアクセストークン」 :4種類の有効期間と 発行数、 再発行、 取り消し - LINE Developers
「チャネルアクセストークンv2.1を :アサーション署名キーと発行する」 JWT - LINE Developers
「Webhookの :HMAC-SHA256、署名を 検証する」 検証の 前に 本文を 変えない こと、 シークレットの 再発行 - LINE Developers
「メッセージ :非同期の(Webhook)を 受信する」 処理、 再送、 webhookEventId、イベントの 種類 - LINE Developers
「Webhookの :2秒のエラーの 原因と 統計情報を 確認する」 request_timeout、原因の 種類、 統計に 検証が 入らない こと - LINE Developers
「ユーザーIDを :開発者本人の取得する」 ユーザーID、 プロバイダーごとに 違う値に なる こと - LINE Developers
「メッセージを :送信の送信する」 種類、 1回に 5つまでの 吹き出し - LINE Developers
「クイックリプライを :ボタンは使う」 13個まで - LINE Developers
「失敗した :APIリクエストを 再試行する」 X-Line-Retry-Key、409、 24時間の 有効期間 - LINE Developers
「ローディングの :表示のアニメーションを 表示する」 秒数と 条件 - LINE Developers
「Messaging APIの :通数の料金」 数え方、 数えられる 送り方、 上限を 超えた とき - LINE Developers
「リッチメニューの :道具ごとの概要」 利点、 1つの メニューに 1つの 道具、 デスクトップ版、 優先順位、 反映の タイミング - LINE Developers
「Messaging API開発ガイドライン」 :禁止事項、ログの 保存 - LINE Developers「FAQ」:応答メッセージが
通数に 数えられない こと、 残りが あるのに 429が 返る 理由 - LINE Developers「LINE Messaging API SDK」:公式の
SDK - LINE Developers
「Messaging APIリファレンス」 :ステータスコード、エラーメッセージ、 応答トークンの 制限、 空の events、ヘッダー名の 扱い、 上限の 目安と 利用状況の API - LINEヤフー for Business
「料金プラン」 :プランごとの内容
よくある質問
LINE Messaging APIは何に使うものですか?
LINE公式アカウントに
チャネルアクセストークンは、どれを使えばよいですか?
LINE Developersの
応答メッセージとプッシュメッセージの違いは何ですか?
応答メッセージは、
メッセージ通数はどう数えられますか?
送信の
Webhook URLの「検証」が失敗するときは、どこを見ればよいですか?
HTTPSで
