LINE APIでできること・連携方法:Messaging API/LINEログイン
2026年時点のLINE APIの全体像と、SDKを使わずに直接呼び出す方法を開発者向けに解説します。各APIでできること、チャネルの準備、fetchでの応答・プッシュ送信、Webhookの署名検証、PKCE対応のLINEログイン、LIFFのIDトークン、レート制限とエラー処理まで。

#LINE APIでできること(2026年版)
LINEが開発者向けに公開している主な仕組みは4つです。LINE公式アカウントとしてメッセージを送受信するMessaging API、自社のWebサイトやアプリにLINEアカウントでログインできるようにするLINEログイン、LINEの中でWebアプリを動かすLIFF(LINE Front-end Framework)、そしてLIFFで作ったアプリをLINE上のサービスとして提供するLINEミニアプリです。この記事では、Node.js 22のfetchでSDKを使わずにHTTPSで直接呼び出す方法と、Python版のWebhookを紹介します。内容は2026年10月時点のLINE Developersのドキュメントで確認しています。
| API | できること | 主な用途 |
|---|---|---|
| Messaging API | 応答メッセージ、プッシュ・マルチキャスト・ブロードキャスト・ナローキャスト送信、リッチメニュー、クイックリプライ、Flex Message、プロフィール取得、オーディエンス、統計、クーポン | チャットボット、注文・予約の通知、カスタマーサポート |
| LINEログイン | OAuth 2.0/OpenID Connectによるログイン、ユーザーID・表示名・プロフィール画像の取得、申請承認後のメールアドレス取得、LINE公式アカウントの友だち追加オプション | 自社サイトやアプリの「LINEでログイン」、会員IDの連携 |
| LIFF | LINE内で開き、ユーザーを識別できるWebアプリ(IDトークン、プロフィール)、開いているトークへのメッセージ送信、シェアターゲットピッカー、QRコード読み取り | フォーム、会員証、LINE内の予約ページ |
| LINEミニアプリ | LIFFアプリをLINE上のサービスとして掲載。2026年3月11日からは台湾とタイで誰でも未認証ミニアプリを公開可能 | ネイティブアプリなしの店舗アプリ、予約、会員サービス |
| LINEソーシャルプラグイン | Webサイト用のシェアボタン・友だち追加ボタン | 記事のシェア、サイトからの友だち獲得 |
一部のAPI(LINE通知メッセージなど)は申請が承認された法人ユーザー専用です。また、古い記事で見かける2つのサービスはすでに終了しています。LINE Notifyは2025年3月31日に終了しました(代わりにLINE公式アカウントからMessaging APIのプッシュメッセージを送ります。詳しくはLINE Notifyの代替手段)。LINE Payのドキュメントも、日本でのLINE Pay終了を受けて2025年4月に提供を終えています。
どのAPIを使えばよいか
- 顧客対応や通知を行うボット → Messaging API
- 自社サイト・アプリのログイン → LINEログイン
- LINE内で開き、ユーザーを識別するWebページ → LIFF(新規のLIFFアプリはLINEミニアプリとして作成することをLINEが推奨しています)
- トークとWeb画面の両方 → 同じプロバイダーでMessaging APIとLIFFを併用
LINE公式アカウントを外部のツールやシステムとつなぐ「API連携」も、基本的にはこのMessaging APIを使います。公式SDKを使いたい場合は、@line/bot-sdk v11で同じWebhookを作る@line/bot-sdk Express middlewareの実装例とMessaging APIでLINE Botを作る手順をご覧ください。
#開発の準備:プロバイダー・チャネル・認証情報
1. 開発者アカウントとプロバイダー LINE Developersコンソールにログインし、サービスを提供する会社や個人を表すプロバイダーを作成します。チャネルはあとから別のプロバイダーに移せず、同じLINEユーザーでもプロバイダーごとに異なるユーザーIDになるため、慎重に選んでください。ユーザーIDを共有したいMessaging APIチャネルとLINEログインチャネルは、同じプロバイダーに置きます。
2. Messaging APIチャネル 2024年9月4日以降、コンソールから直接Messaging APIチャネルは作成できません。先にLINE公式アカウントを作り、LINE Official Account Managerの設定 → Messaging APIで利用を有効にしてプロバイダーを選ぶと、チャネルがコンソールに表示されます。
3. LINEログインチャネル 同じプロバイダーの下にコンソールで作成し、アプリタイプ(ウェブアプリまたはモバイルアプリ)を選んで、LINEログイン設定タブにコールバックURLを登録します。
4. LIFFアプリ LIFFアプリはLINEログインチャネルまたはLINEミニアプリチャネルに追加します(1チャネルあたり最大30個)。Messaging APIチャネルには追加できません。追加するとLIFF IDとLIFF URL(https://liff.line.me/{liffId})が発行されます。
Provider "My Shop"
├── Messaging API channel (LINE公式アカウントと連携)
├── LINE Login channel (Webログイン+LIFFアプリ)
└── LINE MINI App channel (必要に応じて)
| 認証情報 | 確認場所 | 用途 |
|---|---|---|
| チャネルID | チャネル基本設定タブ | LINEログインのclient_id、IDトークンの検証、トークン発行 |
| チャネルシークレット | チャネル基本設定タブ | Webhookの署名検証、LINEログインのトークン交換 |
| チャネルアクセストークン | Messaging API設定タブ(長期)またはAPIで発行 | Messaging API呼び出しのAuthorization: Bearerヘッダー |
| LIFF ID | LINEログイン/ミニアプリチャネルのLIFFタブ | liff.init({ liffId }) |
チャネルアクセストークンは4種類あります。有効期限を指定できるチャネルアクセストークン(v2.1、最長30日、LINEの推奨)、ステートレスチャネルアクセストークン(15分)、短期のチャネルアクセストークン(30日)、そして1チャネルに1つの長期のチャネルアクセストークン(無期限、Messaging APIのみ)です。シークレット類はすべて環境変数やシークレット管理サービスに入れ、サーバー側だけで扱います。
# .env
LINE_CHANNEL_ACCESS_TOKEN=...
LINE_CHANNEL_SECRET=...
LINE_LOGIN_CHANNEL_ID=...
LINE_LOGIN_CHANNEL_SECRET=...
#Messaging API:fetchで応答・プッシュ・マルチキャスト
Messaging APIの呼び出しは、すべてAuthorizationヘッダーにチャネルアクセストークンを付けたhttps://api.line.meへのHTTPSリクエストです。次の小さなヘルパーで、たいていのボットに必要な送信はまかなえます。
// line-api.ts: fetchで作る最小限のMessaging APIクライアント(Node.js 22以降)
const API = "https://api.line.me/v2/bot"
const token = process.env.LINE_CHANNEL_ACCESS_TOKEN
if (!token) throw new Error("Set LINE_CHANNEL_ACCESS_TOKEN")
export type LineMessage = { type: string; [key: string]: unknown }
export class LineApiError extends Error {
constructor(public status: number, public requestId: string | null, public body: string) {
super(`LINE API ${status}: ${body}`)
}
}
async function post(path: string, body: unknown, headers: Record<string, string> = {}) {
const res = await fetch(`${API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, ...headers },
body: JSON.stringify(body),
})
if (!res.ok) throw new LineApiError(res.status, res.headers.get("x-line-request-id"), await res.text())
return res.json() as Promise<{ sentMessages?: { id: string; quoteToken?: string }[] }>
}
// 応答(reply): Webhookイベントの応答トークンが必要。最大5件、メッセージ通数にカウントされない
export function reply(replyToken: string, messages: LineMessage[]) {
return post("/message/reply", { replyToken, messages })
}
// プッシュ: 1人・1グループ・1トークルーム宛て。同じ送信を再試行するときは同じretryKey(UUID)を使う
export function push(to: string, messages: LineMessage[], retryKey: string) {
return post("/message/push", { to, messages }, { "X-Line-Retry-Key": retryKey })
}
// マルチキャスト: 1リクエストあたり最大500ユーザーID
export function multicast(to: string[], messages: LineMessage[]) {
if (to.length > 500) throw new Error("multicast accepts at most 500 user IDs")
return post("/message/multicast", { to, messages })
}
応答(reply)とプッシュの違い 応答メッセージはWebhookイベントに含まれるreplyTokenを使います。トークンは1回限りで、LINEはWebhook受信から約1分以内の使用を求めています(それ以降の動作は保証されません)。応答メッセージは月間のメッセージ通数にカウントされません。一方、プッシュ・マルチキャスト・ブロードキャスト・ナローキャストはカウントされ、1リクエスト内のメッセージオブジェクト数(最大5件)にかかわらず、送信先の人数で数えます。ブロックしたユーザーは数えられません。
| 送信方法 | エンドポイント | 送信先 | 通数へのカウント |
|---|---|---|---|
| 応答 | POST /v2/bot/message/reply | イベントのユーザー・グループ・トークルーム | されない |
| プッシュ | POST /v2/bot/message/push | 1人、1グループ、1トークルーム | される |
| マルチキャスト | POST /v2/bot/message/multicast | 最大500ユーザーID | される |
| ブロードキャスト | POST /v2/bot/message/broadcast | 友だち全員 | される |
| ナローキャスト | POST /v2/bot/message/narrowcast | オーディエンスや属性で絞り込んだ相手 | される |
プッシュメッセージは、友だち、アカウントが参加しているグループやトークルーム、そして友だちでなくても過去7日以内にメッセージを送ってきたユーザーに届きます。ネットワークエラーや5xxで失敗したプッシュを再試行するときは、同じX-Line-Retry-Keyを付けてください。最初のリクエストが受け付け済みなら、LINEは二重送信せずに409を返します。
import { randomUUID } from "node:crypto"
import { push, type LineMessage } from "./line-api.js"
const userId = "U0123456789abcdef0123456789abcdef"
const welcome: LineMessage[] = [
{
type: "text",
text: "ご注文#1042を発送しました。",
quickReply: {
items: [
{ type: "action", action: { type: "uri", label: "配送状況を見る", uri: "https://example.com/track/1042" } },
{ type: "action", action: { type: "message", label: "問い合わせる", text: "注文#1042について質問があります" } },
],
},
},
]
const retryKey = randomUUID()
const result = await push(userId, welcome, retryKey)
console.log(result.sentMessages?.map((m) => m.id))
メッセージはJSONオブジェクトです。よく使う3種類は次のとおりです。
{ "type": "text", "text": "こんにちは!ご用件をお聞かせください。" }
{
"type": "image",
"originalContentUrl": "https://example.com/photo.jpg",
"previewImageUrl": "https://example.com/photo-preview.jpg"
}
{
"type": "flex",
"altText": "週末限定:20%オフ",
"contents": {
"type": "bubble",
"hero": {
"type": "image",
"url": "https://example.com/promo.jpg",
"size": "full",
"aspectRatio": "20:13",
"aspectMode": "cover"
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{ "type": "text", "text": "週末限定キャンペーン", "weight": "bold", "size": "xl" },
{ "type": "text", "text": "日曜日までドリンク全品20%オフ", "wrap": true }
]
},
"footer": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "button",
"style": "primary",
"action": { "type": "uri", "label": "メニューを見る", "uri": "https://example.com/menu" }
}
]
}
}
}
テキストメッセージは最大5,000文字、クイックリプライのボタンは最大13個で、画像やリンクはすべてHTTPSが必要です。毎月の無料メッセージ通数はLINE公式アカウントのプランと国によって異なります(LINE公式アカウントの料金ガイドを参照)。利用状況はGET /v2/bot/message/quotaとGET /v2/bot/message/quota/consumptionで確認できます。
#LINEログイン:PKCE対応の認可コードフロー
LINEログインはOAuth 2.0とOpenID Connectに準拠しています。流れは次のとおりです。
- サーバーがユーザーを
https://access.line.me/oauth2/v2.1/authorizeにリダイレクトします。state(クロスサイトリクエストフォージェリ対策)、nonce(リプレイ攻撃対策)、PKCEのcode_challengeを付けます。 - ユーザーがLINEでログインし、同意画面で許可します。
- LINEがコールバックURLに
codeとstateを付けてリダイレクトします。ユーザーが拒否した場合はerror=ACCESS_DENIEDです。 - サーバーが
POST https://api.line.me/oauth2/v2.1/tokenで認可コードをトークンに交換します。このときcode_verifierを送ります。 - サーバーが
POST https://api.line.me/oauth2/v2.1/verifyでIDトークンをLINEに検証させます。subクレームがユーザーIDです。
// login.ts: Express 5で「LINEでログイン」(認可コードフロー+PKCE)
import express from "express"
import { createHash, randomBytes } from "node:crypto"
const CHANNEL_ID = process.env.LINE_LOGIN_CHANNEL_ID!
const CHANNEL_SECRET = process.env.LINE_LOGIN_CHANNEL_SECRET!
const REDIRECT_URI = "https://example.com/auth/line/callback" // コンソールに登録したコールバックURLと一致させる
const app = express()
// デモ用の保存先。本番ではセッションストアやデータベースを使う
const pending = new Map<string, { nonce: string; codeVerifier: string }>()
function readCookie(header: string | undefined, name: string) {
return header?.split(/;\s*/).find((c) => c.startsWith(`${name}=`))?.slice(name.length + 1)
}
app.get("/auth/line", (_req, res) => {
const state = randomBytes(16).toString("hex")
const nonce = randomBytes(16).toString("hex")
const codeVerifier = randomBytes(32).toString("base64url") // 43文字
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url")
pending.set(state, { nonce, codeVerifier })
res.cookie("line_state", state, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 10 * 60 * 1000 })
const params = new URLSearchParams({
response_type: "code",
client_id: CHANNEL_ID,
redirect_uri: REDIRECT_URI,
state,
scope: "profile openid",
nonce,
code_challenge: codeChallenge,
code_challenge_method: "S256",
})
// URLSearchParamsは空白を"+"にするが、LINEのドキュメントの例は%20
res.redirect(`https://access.line.me/oauth2/v2.1/authorize?${params.toString().replaceAll("+", "%20")}`)
})
app.get("/auth/line/callback", async (req, res) => {
const { code, state, error } = req.query
const saved = typeof state === "string" ? pending.get(state) : undefined
if (typeof state === "string") pending.delete(state)
if (error || typeof code !== "string" || !saved || state !== readCookie(req.headers.cookie, "line_state")) {
res.status(400).send("Login cancelled or invalid state") // ユーザーが拒否するとerror=ACCESS_DENIED
return
}
// 1. 認可コードをトークンに交換
const tokenRes = await fetch("https://api.line.me/oauth2/v2.1/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CHANNEL_ID,
client_secret: CHANNEL_SECRET,
code_verifier: saved.codeVerifier,
}),
})
if (!tokenRes.ok) {
res.status(502).send("Token exchange failed")
return
}
const { access_token, id_token } = (await tokenRes.json()) as { access_token: string; id_token: string }
// 2. IDトークンをLINEで検証(署名・チャネルID・有効期限・nonce)
const verifyRes = await fetch("https://api.line.me/oauth2/v2.1/verify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ id_token, client_id: CHANNEL_ID, nonce: saved.nonce }),
})
if (!verifyRes.ok) {
res.status(401).send("Invalid ID token")
return
}
const claims = (await verifyRes.json()) as { sub: string; name?: string; picture?: string }
// 3. 任意: アクセストークンで最新のプロフィールを取得
const profile = await fetch("https://api.line.me/v2/profile", {
headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json() as Promise<{ userId: string; displayName: string; pictureUrl?: string }>)
// claims.subはこのプロバイダーでのユーザーID。ここでセッションを作成
console.log(claims.sub, profile.displayName)
res.redirect("/")
})
app.listen(3000)
補足:
stateはログインのたびに生成する英数字のランダムな値で、ブラウザに結び付けておきます(この例ではHttpOnlyのCookie)。一致しないコールバックは拒否します。emailスコープは、LINEがチャネルのメールアドレス取得権限の申請を承認したあとでないと使えません。- 認可URLに
bot_prompt=normalまたはbot_prompt=aggressiveを付けると、ログイン時にLINE公式アカウントの友だち追加を表示できます。このときコールバックにはfriendship_status_changedが付きます。
#LIFF:IDトークンをサーバーで検証する
LIFFアプリは、HTTPSのエンドポイントで公開する通常のWebアプリで、LINEのLIFFブラウザで開かれます。npmの@line/liffはバージョン2.31.1以降を使ってください(LINEは2026年9月30日、セキュリティ上の問題によりv2.20.0〜v2.31.0を非推奨にしました)。CDNのhttps://static.line-scdn.net/liff/edge/2/sdk.jsなら、常に最新の2.x系が読み込まれます。
// LIFFアプリ(ブラウザ側): IDトークンを取得してサーバーで検証してもらう
import liff from "@line/liff"
await liff.init({ liffId: "1234567890-AbcdEfgh" })
if (!liff.isLoggedIn()) {
liff.login() // 外部ブラウザで必要。LIFFブラウザではログイン済み
} else {
const idToken = liff.getIDToken() // openidスコープが必要
await fetch("/api/me", { method: "POST", headers: { Authorization: `Bearer ${idToken}` } })
}
liff.getProfile()の結果を本人確認の根拠としてサーバーに送ってはいけません。リクエストボディは簡単に偽造できるためです。IDトークン(またはアクセストークン)を送り、LINEログインの節と同じPOST /oauth2/v2.1/verifyでサーバー側で検証します。このときclient_idには、LIFFアプリを追加したLINEログインチャネルのチャネルIDを指定します。画面サイズ、liff.sendMessages()、シェアターゲットピッカー、外部ブラウザでの動作、デプロイについてはLIFFアプリ開発ガイドで詳しく解説しています。
#Webhook:署名検証とイベント処理
トークで何かが起きると(メッセージの受信、友だち追加、ポストバックボタンのタップなど)、LINEはMessaging APIチャネルに1つだけ設定できるWebhook URLへHTTPS POSTを送ります。コンソールのMessaging API設定タブでURLを登録し、検証を押してからWebhookの利用をオンにします。URLには一般的なブラウザが信頼する認証局の証明書が必要で、自己署名証明書は使えません。サーバーで返信する場合は、LINE Official Account Managerであいさつメッセージと応答メッセージをオフにしておくと、ユーザーに返信が2通届くのを防げます。
LINEはすべてのリクエストに署名を付けます。x-line-signatureヘッダーは、チャネルシークレットをキーにした生のリクエストボディのHMAC-SHA256をBase64エンコードした値です。JSONとして解析する前に、受信したバイト列そのもので検証してください。express.json()のあとにJSON.stringify(req.body)のように解析・再シリアライズすると、バイト列が変わって検証に失敗します。
// webhook.ts: SDKを使わずにイベントを受信(Express 5)
import express from "express"
import { createHmac, timingSafeEqual } from "node:crypto"
import { reply } from "./line-api.js"
const CHANNEL_SECRET = process.env.LINE_CHANNEL_SECRET!
const app = express()
function isValidSignature(rawBody: Buffer, signature: string | undefined) {
if (!signature) return false
const expected = createHmac("sha256", CHANNEL_SECRET).update(rawBody).digest()
const received = Buffer.from(signature, "base64")
return received.length === expected.length && timingSafeEqual(received, expected)
}
type LineEvent = {
type: string
webhookEventId: string
deliveryContext: { isRedelivery: boolean }
replyToken?: string
source?: { type: string; userId?: string; groupId?: string; roomId?: string }
message?: { id: string; type: string; text?: string }
postback?: { data: string }
}
// express.rawでLINEが署名したバイト列をそのまま保持。このルートでexpress.json()は使わない
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body
if (!Buffer.isBuffer(rawBody) || !isValidSignature(rawBody, req.get("x-line-signature"))) {
res.sendStatus(401)
return
}
const { events } = JSON.parse(rawBody.toString("utf8")) as { events: LineEvent[] }
res.sendStatus(200) // すぐに応答する。eventsが[](疎通確認)のときも同じ
for (const event of events) handleEvent(event).catch((err) => console.error(err))
})
const seen = new Set<string>() // 本番ではTTL付きのRedisやデータベースを使う
async function handleEvent(event: LineEvent) {
if (seen.has(event.webhookEventId)) return // 再送または重複したイベント
seen.add(event.webhookEventId)
switch (event.type) {
case "message":
if (event.replyToken && event.message?.type === "text") {
await reply(event.replyToken, [{ type: "text", text: `「${event.message.text}」と受け取りました` }])
}
break
case "follow":
if (event.replyToken) await reply(event.replyToken, [{ type: "text", text: "友だち追加ありがとうございます!" }])
break
case "postback": {
const data = new URLSearchParams(event.postback?.data) // 例: "action=book&slot=10:00"
console.log("postback", data.get("action"), data.get("slot"))
break
}
case "unfollow":
// 応答トークンなし: ブロックされたので event.source?.userId への送信を止める
break
}
}
app.listen(Number(process.env.PORT ?? 3000))
このコードが従っているLINEのドキュメント上のポイント:
- 正しいリクエストにはすべて
200を返します。LINEが疎通確認のために送る"events": []のリクエストも同じです。 - イベントは非同期で処理し、遅い処理が後続のWebhookを待たせないようにします。レスポンス送信後に処理を止めるサーバーレス環境では、応答する前にイベントをキューに入れます。
- LINEは送信元のIPアドレスを公開していないため、認証の手段は署名検証です。
- Webhookの再送はデフォルトでオフです。コンソールで有効にすると、2xxを返さなかったWebhookが再送され、同じイベントが2回届くこともあります。処理済みの
webhookEventIdは読み飛ばし、deliveryContext.isRedeliveryで再送かどうかを判別します。
| イベント | 発生するとき | 応答トークン |
|---|---|---|
message | ユーザーがテキスト・画像・動画・音声・ファイル・位置情報・スタンプを送信 | あり |
follow | 友だち追加またはブロック解除 | あり |
unfollow | ブロック | なし |
postback | ポストバックアクション(ボタン、リッチメニュー、クイックリプライ)のタップ | あり |
unsend | ユーザーが送信を取り消し | なし |
join / leave | アカウントがグループ・トークルームに参加/退出・削除 | join:あり、leave:なし |
memberJoined / memberLeft | アカウントのいるグループにメンバーが参加/退出 | joined:あり、left:なし |
messageEdited | グループトークでユーザーがテキストを編集(2026年8月12日追加) | あり |
videoPlayComplete、beacon、accountLink、membership | 動画の最後までの視聴、LINE Beacon、アカウント連携、メンバーシップ | あり |
同じWebhookを公式SDK(line-bot-sdk v3、Python 3.10以降)で書くと次のようになります。WebhookHandler.handle()が署名を検証し、不一致ならInvalidSignatureErrorを送出します。
# app.py: 同じWebhookをPythonで(line-bot-sdk v3、Python 3.10以降)
import os
from flask import Flask, request, abort
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 FollowEvent, 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(401)
return "OK"
@handler.add(MessageEvent, message=TextMessageContent)
def on_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}」と受け取りました")],
)
)
@handler.add(FollowEvent)
def on_follow(event):
with ApiClient(configuration) as api_client:
MessagingApi(api_client).reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text="友だち追加ありがとうございます!")],
)
)
#セキュリティ・レート制限・エラー処理
セキュリティチェックリスト
- すべてのWebhookで、生のボディに対して署名を検証し、比較は定数時間で行う。
- チャネルシークレットとトークンは環境変数やシークレット管理サービスに置いてサーバー側だけで扱い、漏えいが疑われるトークンは失効させる。
- チャネルアクセストークンv2.1や短期のトークンを基本にする。長期のトークンを使う場合は、コンソールのセキュリティ設定タブでAPIを呼び出せるIPアドレスを制限する。
- LINEログインとLIFFのIDトークンはサーバーで検証し、ブラウザから送られたユーザーIDを信用しない。
- すべてHTTPSで通信し、ユーザー入力は保存・表示の前に検証・エスケープする。
- エンドポイントのパスはリファレンスどおりに指定する。末尾のスラッシュなどの違いは動作が保証されないと、LINEが2026年8月17日に注意喚起しています。
レート制限(チャネル単位、2026年10月時点)
| エンドポイント | 上限 |
|---|---|
| 応答、プッシュなど大半のエンドポイント | 毎秒2,000リクエスト |
| マルチキャスト(2025年4月23日から) | 毎秒200リクエスト |
| リッチメニューの一覧取得(2026年5月26日から) | 毎秒10リクエスト |
| ローディングのアニメーション表示 | 毎秒100リクエスト |
| ブロードキャスト、ナローキャスト、統計、Webhookエンドポイントのテスト | 1時間に60リクエスト |
| APIでのリッチメニュー作成・削除 | 1時間に100リクエスト |
エラーはmessageと、不正なリクエストの場合はdetails配列を含むJSONで返ります。
| ステータス | 意味 | 対処 |
|---|---|---|
| 400 | 不正なリクエスト、またはInvalid reply token(使用済み・期限切れのトークン) | detailsを見てペイロードを直す。そのまま再試行しない |
| 401 | チャネルアクセストークンが無効 | トークンを発行し直す |
| 403 | アカウントやプランでこのエンドポイントを使えない | プランと権限を確認 |
| 409 | 同じX-Line-Retry-Keyのリクエストが受け付け済み | 成功として扱う |
| 413 | リクエストが2MBを超えている | 1回に送る量を減らす |
| 429 | レート制限超過、または月間の上限に到達("You have reached your monthly limit.") | 間隔を空けて再試行し、通数とプランを確認 |
| 500 | LINE側のエラー | 間隔を空けて再試行(プッシュは同じリトライキーで) |
import { randomUUID } from "node:crypto"
import { LineApiError, push, type LineMessage } from "./line-api.js"
export async function pushWithRetry(to: string, messages: LineMessage[], attempts = 3) {
const retryKey = randomUUID() // すべての再試行で同じキーを使う
for (let attempt = 1; ; attempt++) {
try {
return await push(to, messages, retryKey)
} catch (err) {
if (err instanceof LineApiError && err.status === 409) return // 以前のリクエストで受け付け済み
const retryable = !(err instanceof LineApiError) || err.status === 429 || err.status >= 500
if (!retryable || attempt >= attempts) throw err
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000))
}
}
}
レスポンスヘッダーのx-line-request-idもログに残してください(上の例ではLineApiError.requestIdに保持)。問題をLINEに問い合わせる際に求められます。
#LineBot.proが役立つ場面
LineBot.proはSDKでもAPI提供サービスでもなく、公開APIもありません。この記事の内容は、すべて自社のコードと自社のチャネルで動きます。そのうえで、時間を節約できる場面が2つあります。
- メッセージとメニューのデザイン AIジェネレーターで、リッチメッセージ(ヒーロー画像・見出し・本文・ボタンを持つFlexバブル)や、タップ領域1〜6個のリッチメニュー画像を作れます。Flex JSONをコピーして上の
replyやpushにそのまま渡すか、メニューの画像とJSONをダウンロードして自分でアップロードします。リッチメッセージは8クレジット、リッチメニューは10クレジットで、新規登録で50クレジットが無料で付きます。クレジットは期限のない買い切りパックです(料金)。Flex Messageを作成する - コードを書かないボット キーワードへの返信で足りるなら、LINEチャットボットツールが1対1のトークで、キーワードに一致したメッセージに承認済みのリッチメッセージカードで返信します。一致しないメッセージには、オプションでAIによる意図の振り分けも使えます(確認したメッセージ1件につき1クレジット。AIはカードを選ぶだけで、回答文は書きません)。このツールは独自のWebhook URLを使い、1チャネルに設定できるWebhook URLは1つだけなので、そのチャネルでは自社のWebhookサーバーと併用できず、置き換える形になります。
自社システムとのMessaging API連携や、LIFFアプリ・LINEミニアプリの開発は、LINE開発の受託とLINEアプリ開発で個別にお受けしています。用途を添えてお問い合わせください。
#よくある質問
Q: LINE APIでは何ができますか? Messaging APIを使うと、LINE公式アカウントからユーザーへの応答、プッシュ・マルチキャスト・ブロードキャスト・ナローキャストの送信、リッチメニューの管理、プロフィールや統計の取得ができます。LINEログインはWebサイトやアプリに「LINEでログイン」を追加し、LIFFはLINE内でWebアプリを動かし、LINEミニアプリはそのWebアプリをLINE上のサービスとして公開します。
Q: LINE Messaging APIは無料で使えますか? API呼び出し自体は無料です。応答メッセージは通数にカウントされず、プッシュ・マルチキャスト・ブロードキャスト・ナローキャストは送信先1人につき1通としてプランの月間通数にカウントされます。2026年10月時点で、無料プランの月間通数は日本200通、台湾200通、タイ300通です。有料プランは料金ガイドをご覧ください。
Q: Webhookの署名検証が失敗するのはなぜですか?
多くの場合、検証前にボディがJSONとして解析され(express.json()など)、LINEが署名したものと異なるバイト列でハッシュを計算しているためです。ほかに、別チャネルのシークレットや再発行前のシークレットを使っている、プロキシがボディを書き換えている、アルゴリズムが違う、といった原因があります。生のボディをHMAC-SHA256で計算し、Base64で比較してください。
Q: LINE Notifyはまだ使えますか? 使えません。LINE Notifyは2025年3月31日に終了しました。通知はLINE公式アカウントからMessaging APIのプッシュメッセージで送ります。受信するユーザーはそのアカウントの友だちである必要があります。
Q: LINE APIを使うのにSDKは必要ですか? 必須ではありません。この記事のとおり、どのAPIもHTTPSとJSONで呼び出せます。公式SDK(Node.js、Python、PHP、Java、Go、Ruby)を使うと、型定義や署名検証の関数、正しいエンドポイントパスが手に入り、ほかの言語向けにはOpenAPI仕様が公開されています。
Q: LIFFとLINEログインの違いは何ですか? LINEログインは、LINEの外にある自社サイトやアプリにユーザーをログインさせる仕組みです。LIFFは自社のWebアプリをLINEの中で動かす仕組みで、内部ではLINEログインを使っています。そのためLIFFアプリはLINEログインチャネル(またはLINEミニアプリチャネル)に追加します。
次のステップ:
- 同じボットをSDKで作る:@line/bot-sdk Express middlewareの実装例
- 会話フローまで作り込む:Messaging APIでLINE Botを作る手順
- LINE内のWeb画面を作る:LIFFアプリ開発ガイド
- プラットフォームの変更点を確認する:LINE Messaging APIの2026年アップデート
- 開発を任せたい:LINE開発の受託
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と引用トークン、実際に出るエラーと対処を解説します。
続きを読む
LIFFアプリ開発ガイド2026:Next.js・TypeScriptで作る
2026年版のLIFF(LINE Front-end Framework)アプリ開発ガイドです。チャネルとエンドポイントURLの設定、liff.initのルール、サーバー側でのIDトークン検証、sendMessages、shareTargetPicker、ディープリンク、LIFF CLIでのテスト、v2.31.1のセキュリティ更新まで解説します。
続きを読む
LINE Botの作り方 2026:Messaging API開発入門
Messaging APIでLINE Botを作る手順を、チャネルの準備からWebhookの署名検証、Flexメッセージとクイックリプライ、ポストバックを使った予約フロー、FAQに限定したAI回答、デプロイまで解説します。Node.js(@line/bot-sdk v11)とPython(line-bot-sdk v3)のコード付きです。
続きを読む