LINEミニアプリの作り方 予約画面を作る手順
LINEミニアプリで
このliff.init()で
この
この
載せた
LINEミニアプリは、LIFFの上で動くWebアプリ
LINEミニアプリは、
LINE Developersの
もう
LINEミニアプリは、
| 未認証ミニアプリ | 認証済ミニアプリ | |
|---|---|---|
| なり方 | チャネルを |
LINEヤフーの |
| ヘッダーの |
タイトルと |
認証バッジ、 |
| サービスメッセージ | 開発用の |
本番で |
| ホーム画面への |
使えない | 使える |
| Custom Path |
使えない | 申請して |
| チャネル同意の |
開発用と |
動く |
| LINEの |
出ない | 出る |
LIFFアプリとの
- アクションボタンは
消せない。 ヘッダーのアクションボタン (シェアなど)は 常に 出ます。 LIFFの モジュールモードは 使えません - 画面サイズは
Fullだけ。 下から少しだけ出る 形は 選べません - 1つの
チャネルに 予約、Webアプリは 1つ。 会員証、 問い合わせのように 画面を 分けたい ときは、 1つの Webアプリの 中で ページを 分けます
予約画面は7つの手順で作る
作る
| 手順 | すること | 詰まる所 |
|---|---|---|
| 1 | LINEミニアプリチャネルを |
プロバイダーは |
| 2 | 3つの |
内部チャネルごとに |
| 3 | liff.init()で始める |
URLのinitが |
| 4 | IDトークンを |
プロフィールを |
| 5 | 予約の |
認証済だけ。 |
| 6 | パーマネントリンクで |
LIFF URLではなく、 |
| 7 | 審査を |
1〜2週間かかる。 |
手順1. チャネルは、コンソールで「LINEミニアプリ」として作る
LINEミニアプリチャネルは、
- LINE Developersコンソールで、
プロバイダーを 選ぶ - [チャネル設定]
>[新規チャネル作成] >[LINEミニアプリ]の 順に 押す - 次の
項目を 入れる - 会社を
代表する 権限が ある ことの 表明に チェックを 付け、 [作成]を 押す - 「情報利用に
関する 同意に ついて」を 読み、 [同意する]を 押す
| 項目 | 必須 | 予約画面で |
|---|---|---|
| サービスを |
必須 | 日本、 |
| チャネルアイコン | 任意 | 同意の |
| チャネル名 | 必須 | 「LINE」や |
| チャネル説明 | 必須 | 審査は |
| メールアドレス | 必須 | 審査の |
| プライバシーポリシーURL | 説明を参照 | 作る |
| サービス利用規約URL | 任意 | 同意の |
| 規約への |
必須 | 所在国・地域は、 |
チャネルを
な
いちばん
チャネル説明は、
手順2. 3つの内部チャネルにURLを置き、手元で開く
コンソールでは
| 内部チャネル | 使う人 | ステータス | 置くもの |
|---|---|---|---|
| 開発用 | 権限を |
常に |
作っている |
| 審査用 | LINEヤフーの |
常に |
審査に |
| 本番用 | お客様 | 常に |
公開する版 |
内部チャネルごとにhttpsで始め、#は
予約画面の
開発用 https://dev.example.com/booking
審査用 https://review.example.com/booking
本番用 https://www.example.com/booking開発用と
設定が
- 未認証ミニアプリの
間は、 コンソールで設定すると、 開発用の 内容が 本番用にも 写ります。 ただし、 サービスメッセージの テンプレートと チャネル同意の 簡略化は、 審査に 通るまで 写りません - 認証済ミニアプリになった
後は、 コンソールの変更は 開発用だけに 入ります。 審査用には 審査の 開始時に、 本番用には 公開の 時に 写ります
未認証の
手元で開くときは、LIFF CLIのserveを開発用だけに使う
LINEのserveコマンドが
npm install -g @line/liff-cli
mkcert -install # 最初の1回だけ。手元に認証局を作る
mkcert localhost # localhost.pem と localhost-key.pem を作る
liff-cli channel add <チャネルID> # チャネルシークレットを聞かれる
liff-cli serve --liff-id <開発用のLIFF ID> --url http://localhost:3000/localhost.pemとlocalhost-key.pemをserveを--proxy-type ngrokとNGROK_AUTHTOKENの
ドキュメントは、serveをchannel addとserveが
ひな形がnpx @line/create-liff-appで
手順3. liff.init()は、ページを開くたびに最初に呼ぶ
SDKは、@line/liffかliff.init()は、
// main.js:予約画面の入口。ページを開くたびに liff.init() を呼ぶ
import liff from "@line/liff";
// 内部チャネル(開発用・審査用・本番用)ごとにLIFF IDが違うので、ビルドの環境変数で切り替える
const LIFF_ID = import.meta.env.VITE_LIFF_ID;
async function start() {
try {
await liff.init({ liffId: LIFF_ID });
} catch (err) {
showError("画面を開けませんでした。LINEから開き直してください。");
console.error(err.code, err.message);
return;
}
// URLの書き換えや画面の切り替えは、init が終わってから行う
renderBookingForm();
}
start();showErrorとrenderBookingFormは、liff.init()に
initの
- エンドポイントURLの
下で 動きが動かす。 保証されるのは、 エンドポイントURLと 同じか、 その 下の 階層の URLだけです。 https://example.com/bookingなら、/booking/slots/は対象で、/や/other/は対象外です - URLの
書き換えは initの後。history.pushState()などでの書き換えや、 サーバーでの リダイレクトは、 liff.init()が終わってから 行います - 最初の
URLを 最初にログに 送らない。 開いた URLには お客様の アクセストークンが 入ります。 v2.11.0以降は initの完了時に URLから 取り除かれるので、 アクセス解析の ページビューは initの後に 送ります
LIFFブラウザのinitでliff.login()はwithLoginOnExternalBrowser: trueを
会社サイトの
手順4. サーバーにはIDトークンを送り、検証してから予約を受ける
予約のliff.getProfile()やliff.getDecodedIDToken()で
サーバーには、liff.getIDToken()で
// submit.js:予約を送る。プロフィールではなくトークンをサーバーへ渡す
import liff from "@line/liff";
export async function submitBooking(form) {
// 名前を使うなら、トークンを取る前に profile の権限を確かめる
const status = await liff.permission.query("profile");
if (status.state === "prompt") {
await liff.permission.requestAll();
}
const idToken = liff.getIDToken(); // openid の権限が要る。取れないときに備えて下で確かめる
const liffAccessToken = liff.getAccessToken(); // サービスメッセージ用
if (!idToken) throw new Error("no id token");
const res = await fetch("/api/bookings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
idToken,
liffAccessToken,
slotId: form.slotId,
phone: form.phone,
}),
});
if (!res.ok) throw new Error(`booking failed: ${res.status}`);
return res.json();
}最初にopenid)だけです。profileは、liff.permission.query()とliff.permission.requestAll()でrequestAll()はquery()でpromptか
もうsubmit.jsで、liff.getAccessToken()を
予約のliff.getIDToken()はemailのemailを
サーバーの
// verify.mjs:IDトークンをLINEのエンドポイントで検証し、ユーザーIDを取り出す
const CHANNEL_ID = process.env.LINE_MINIAPP_CHANNEL_ID; // 内部チャネルごとに確かめる
export async function verifyIdToken(idToken) {
const res = 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: idToken, client_id: CHANNEL_ID }),
});
const body = await res.json();
if (!res.ok || body.error) {
// 例:{ "error": "invalid_request", "error_description": "IdToken expired." }
throw new Error(`verify failed: ${res.status} ${body.error_description}`);
}
if (body.aud !== CHANNEL_ID) throw new Error("aud mismatch");
return { userId: body.sub, name: body.name ?? null }; // name は profile の同意がないと入らない
}client_idには、sub(ユーザーID)、aud(チャネルID)、exp(有効期限)などがnameとpictureはprofile、emailはemailのerror_descriptionに
error_description |
意味 | 見直すこと |
|---|---|---|
Invalid IdToken. |
形が |
画面から |
Invalid IdToken Issuer. |
LINE以外で |
送られてきた |
IdToken expired. |
有効期限切れ | IDトークンは |
Invalid IdToken Audience. |
client_idと合わない |
内部チャネルの |
予約のsubを
手順5. 予約の確認は、サービスメッセージで送る
サービスメッセージは、
送れる
| 例 | |
|---|---|
| 送れる |
予約を |
| 送れる |
チェックインが |
| 送れる |
予約の |
| 送れない | 値下げ、 |
| 送れない | LINEミニアプリの |
決まりに
送るまでの流れ
- コンソールの
[サービスメッセージテンプレート] タブで、 予約などの カテゴリから テンプレートを 選んで 追加する (1チャネルに 20個まで)。 [使用事例]には 送る 場面を 正しく 書く - 予約の
ときに、 画面で liff.getAccessToken()を取り、 サーバーへ 送る (上の submit.js) - サーバーで、
チャネルアクセストークンを 発行する - LIFFの
アクセストークンを 使って、 サービス通知トークンを 発行する - サービス通知トークンで
1通目を 送り、 返ってきた 新しい トークンを 保存する - 前日の
リマインドなど後の 連絡は、 保存した トークンで 送り、 また 新しい トークンを 保存する
チャネルアクセストークンは、
// notify.mjs:予約を受けた直後に、サービスメッセージで確認を送る
const API = "https://api.line.me";
async function statelessToken() {
const res = await fetch(`${API}/oauth2/v3/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.LINE_MINIAPP_CHANNEL_ID,
client_secret: process.env.LINE_MINIAPP_CHANNEL_SECRET,
}),
});
if (!res.ok) throw new Error(`token ${res.status}`);
return (await res.json()).access_token; // 15分だけ有効。使うたびに発行してよい
}
async function post(path, token, payload) {
const res = await fetch(`${API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
});
const body = await res.json();
if (!res.ok) throw new Error(`${path} ${res.status} ${body.message}`);
return body;
}
// 1回目:LIFFのアクセストークンから、サービス通知トークンを1つだけ発行して送る
export async function sendConfirmation(liffAccessToken, params) {
const token = await statelessToken();
const issued = await post("/message/v3/notifier/token", token, { liffAccessToken });
const sent = await post("/message/v3/notifier/send?target=service", token, {
templateName: process.env.SERVICE_TEMPLATE_CONFIRM, // 例の形:{テンプレート名}_ja
params, // キーはテンプレートごとに決まっている
notificationToken: issued.notificationToken,
});
// 次の連絡(前日のリマインドなど)に使うので、予約の行に保存する
return { notificationToken: sent.notificationToken, remainingCount: sent.remainingCount };
}
// 2回目以降:保存しておいた最新のサービス通知トークンで送る
export async function sendFollowUp(saved, templateName, params) {
if (!saved.notificationToken || saved.remainingCount <= 0) return null;
const token = await statelessToken();
const sent = await post("/message/v3/notifier/send?target=service", token, {
templateName,
params,
notificationToken: saved.notificationToken,
});
return { notificationToken: sent.notificationToken, remainingCount: sent.remainingCount };
}.envには
VITE_LIFF_ID=
LINE_MINIAPP_CHANNEL_ID=
LINE_MINIAPP_CHANNEL_SECRET=
SERVICE_TEMPLATE_CONFIRM=
SERVICE_TEMPLATE_REMIND=テンプレートの{template name}_{BCP 47 language tag}の_jaです。paramsの
サービス通知トークンの
- 1つの
操作で トークンは送れるのは 5回まで。 発行から 1年間有効で、 残りの 回数は remainingCountで分かります - 送る
たびに 返ってきたトークンが 入れ替わる。 notificationTokenを、予約の 行に 上 書きして 保存します。 expiresInとremainingCountが0で返った ときは、 送れたけれどトークンを 更新できなかった、と いう 意味です - 1つの
LIFFの 後のアクセストークンで 発行できるのは 1つだけ。 連絡の ために、 同じ アクセストークンから 発行し直してはいけません。 短い間に 続けて 発行を 頼むと 400に なります - LIFFの
アクセストークンは 有効期間は早めに 使う。 12時間ですが、 お客様が 画面を 閉じると 無効に なることがあります。 追加の 権限を 許可して もらった ときも、 それより 前の トークンは 無効に なります。 予約を 送った 処理の 中で、 すぐに 1通目まで 送ります - トークンは
1人に ある結びつく。 人の トークンで、 別の 人には 送れません
前日のsendFollowUpを
未認証ミニアプリの
手順6. 共有は、パーマネントリンクで行う
予約の
パーマネントリンクは、
パーマネントリンク = LIFF URL +(開きたいページのURL − エンドポイントURL)
エンドポイントURL https://www.example.com/booking
開きたいページ https://www.example.com/booking/fair?date=2026-12-06
LIFF URL https://miniapp.line.me/<本番用のLIFF ID>
パーマネントリンク https://miniapp.line.me/<本番用のLIFF ID>/fair?date=2026-12-06画面のliff.permanentLink.createUrlBy()が/が/をcreateUrlBy()でhttps://miniapp.line.me/{liffId}のhttps://liff.line.me/{liffId}でも
気をhttps://miniapp.line.me/<独自の文字列>の
LINEを
手順7. 審査の前に用意しておくこと
認証済ミニアプリに
申請の
| 用意する |
どこに | 予約画面での |
|---|---|---|
| チャネル説明 | [チャネル基本設定] | 何を |
| プロバイダー名 | [プロバイダー設定] | サービスを |
| プライバシーポリシーURL | [チャネル基本設定] | ユーザー情報を |
| 審査用と |
各エンドポイントURL | 審査用の |
| サービス事業主名の |
画面の中 | 運営しているのが |
| 問い合わせ先 |
画面の中 | 問い合わせの |
| トップページ | エンドポイントURL | エラーや |
| 外部ブラウザからも |
エンドポイントURL | Safariや |
| テストシナリオ | [審査の |
予約を |
| ベーシック認証の |
[審査の |
審査用の |
| 各ガイドラインへの |
画面と |
アイコンの |
サービス利用規約の
LINEミニアプリポリシー
審査が
審査に通った後
初めて
公開した
業種ごとに変わるのは、入力の項目とリマインドの数
7つの
| 業種 | 入力して |
サービスメッセージで |
公式アカウントで |
|---|---|---|---|
| ジム・スタジオ | 体験か |
予約の |
新しい |
| 整体院 | 初めてかどうか、 |
予約の |
休診日の |
| 結婚 |
希望の |
予約の |
次の |
1つの
会社サイトの
やってはいけないこと
作る
getProfile()のユーザーIDを、 サーバーにはそのまま サーバーで 信じる。 IDトークンを 送り、 検証して 得た subを使います- 本番用の
チャネルで エンドポイントURLがliff-cli serveを動かす。書き換わり、 お客様が 開けなくなります - LIFF IDや
チャネルアクセストークンを、 開発用の内部チャネルの 間で 使い回す。 トークンで、 審査用や 本番用から 送らないよう 書かれています - サービスメッセージで、
次の APIをフェアや キャンペーンを 案内する。 一定期間 使えなくなる ことがあります。 宣伝は 公式アカウントから 送ります - 後の
連絡の 返ってきたために、 同じ LIFFの アクセストークンで サービス通知トークンを 発行し直す。 新しい トークンを 保存して 使います - チャネルシークレットを、
画面の ステートレスのコードや gitに 入れる。 トークンを 発行するのは サーバーだけに します - 最初の
URLを、 アクセストークンがアクセス解析に そのまま 送る。 含まれます。 ページビューは initの後に 送ります
公開前に確かめること
審査に
- LINEミニアプリチャネルが、
公式アカウントの Messaging APIの チャネルと 同じ プロバイダーに ある - 開発用・審査用・本番用の
LIFF IDと エンドポイントURLを、 表に してある - それぞれの
内部チャネルで、 liff.init()に渡すLIFF IDが 合っている - サーバーが、
IDトークンを 検証してから ユーザーIDを 使っている - 名前を
使うなら、 profileの同意を 求めてから トークンを 取っている - サービスメッセージの
テンプレートを 追加し、 [使用事例]に 送る 場面を 書いた - 予約の
行に、 最新の サービス通知トークンと 残りの 回数を 保存している - チラシやリッチメニューの
リンクが、 本番用の パーマネントリンクに なっている - 事業主の
名前、 問い合わせ先、 プライバシーポリシーへの リンクが 画面に ある - Safariや
Chromeで 開いても、 トップページが 出る - 審査の
ための 補足資料に、 予約の テストシナリオを 書いた - 予約の
画面を 最初から 送信の 完了まで、 iPhoneと Androidの 実機で 通した - 予約の
確認が、 お客様の 側と 店の 側の 両方に 届く (店への 連絡は メールや 管理画面で 別に 持つ) - シークレットと
トークンが gitの 履歴に 残っていない
確かめられなかったこと
この
- 本物の
LINEとの やり取りの すべて。 チャネルの 作成、 内部チャネルの 画面、 liff.init()、IDトークンの 検証、 サービスメッセージの 送信は 試していません。 コードは node --checkで構文を 確かめただけです - 2025年2月12日の
ニュース (LIFFと LINEミニアプリの ブランド統合)と、 2025年9月26日の ニュース (外部ブラウザでの 利用)の 本文。 どちらも ページを 開けず、 ほかの ページに 書かれた 要約だけを 使いました。 統合の 時期と、 既存の LIFFアプリへの 影響は 分かっていません - IDトークンの
検証に 失敗した ときの HTTPの ステータスコード。 リファレンスには、 失敗すると JSONが 返る ことと error_descriptionの一覧は 書かれていますが、 ステータスコードの 記載は 見つけられませんでした。 コードでは errorの有無も 見ています - 3つの
内部チャネルで、 チャネルIDと チャネルシークレットが 別の 値に なるか。 チャネルアクセストークンを 内部チャネルごとに 発行するよう 書かれているので、 別の 値と して 扱う 前提で コードを 書いています - LIFF CLIの
channel addとserveが、LINEミニアプリの 開発用の 内部チャネルで そのまま 使えるか - 予約の
カテゴリの サービスメッセージの テンプレート名と、 paramsのキー - エンドポイントURLの
末尾に /がある ときの、 パーマネントリンクの 作られ方 (ドキュメントの 例は 末尾に /がない 形だけ) - 認証済ミニアプリに、
サービス利用規約の URLが 要るか どうか。 確かめた 範囲では 任意の 項目でした - LINEを
入れていない 人が 外部ブラウザで 開いた ときの、 2025年10月1日以降の 画面
自社でやる場合と、任せる場合
予約の
任せる
出典(一次情報)
確認日は
- LINE Developers
「LINEミニアプリとは」 :LIFFの上で 動く こと、 未認証と 認証済の 違い、 LIFFアプリで できて LINEミニアプリで できない こと、 ブランド統合の 予定と 新規作成の 勧め - LINE Developers「はじめに
(LINEミニアプリ)」 :開発から公開までの 流れ - LINE Developers
「開発を :チャネルの始めよう」 作り方と 入力項目、 プライバシーポリシーURL、 プロバイダーの 注意、 内部チャネル、 ベーシック認証の 条件 - LINE Developers
「LINEミニアプリ用LINE Developersコンソールガイド」 :3つの 内部チャネル、 LIFF IDと エンドポイントURL、 トークンの 種類、 設定が 写る タイミング、 チャネル説明 - LINE Developers
「LINEミニアプリの :HTML5、仕様」 LIFFの 対応バージョン、 2025年10月1日からの 外部ブラウザでの 利用 - LINE Developers
「LINEミニアプリの :チャネル名、ユーザーに 表示される 設定一覧」 チャネル説明、 プライバシーポリシーURLが 出る 画面、 チャネル名と 説明は 英語で 入れ日本語は [多言語対応]で 入れる こと - LINE Developers
「LINEミニアプリの :チャネル同意の認可フロー」 簡略化、 2026年1月8日以降の 新規チャネル、 動作条件 (認証済・v2.13.x以降)、 IDトークンに 名前が 入らない 場合と liff.permission、権限ごとの メソッド - LINE Developers
「サービスメッセージを :認証済のみ、送信する」 テンプレートの 追加と [使用事例]、 送る 流れ、 トークンの 保存、 5回と 1年、 使える トークン - LINE Developers
「サービス事業主の :パーマネントリンクをための ノウハウ」 使う 場面、 サービスメッセージで 許可・ 禁止されている 通知、 違反した とき - LINE Developers
「LINEミニアプリ APIリファレンス」 :サービス通知トークンの発行と サービスメッセージの 送信、 エラー - LINE Developers
「パーマネントリンクを :リンクの作成する」 式、 miniapp.line.meへの変更、 LINEを 入れていない 場合 - LINE Developers
「Custom Pathを :認証済のみ、設定する」 申請から 1〜2週間、 変更できない こと - LINE Developers
「カスタムアクションボタンを :ヘッダーの実装する」 アクションボタン、 カスタムシェアメッセージの ガイドライン - LINE Developers
「審査を :申請前の依頼する」 確認、 1〜2週間、 テストシナリオ、 承認後の 公開と 30日の 自動処理 - LINEミニアプリポリシー:認証済の
対象者、 外への 誘導、 外部ブラウザ、 企業に 関する 情報、 トップページ、 推奨事項 (リダイレクト、 事業主名と 問い合わせ先の 表示) (最終改定日 2026年10月14日) - LINE Developers
「LIFFの :推奨環境、概要」 LIFFブラウザ、 開発の ツール - LINE Developers
「LIFFアプリを :LINEログインのチャネルに 追加する」 チャネルへの 追加、 エンドポイントURLの 条件、 Scope - LINE Developers
「LIFFアプリを :SDKの開発する」 読み込み、 liff.init()の注意、 ログイン、 liff.permanentLink.createUrlBy() - LINE Developers
「LIFFアプリおよびサーバーで :プロフィールをユーザー情報を 使用する」 サーバーへ 送らない こと、 IDトークンの 検証 - LINE Developers「LIFF CLI」:インストール、
serve、mkcert、 ngrok、 公開済みの アプリで 使わない こと - LINE Developers
「Create LIFF Appで :ひな形のLIFFアプリの 開発環境を 構築する」 作り方 - LINE Developers
「LIFFリリースノート」 :2020年2月5日からMessaging APIの チャネルに LIFFアプリを 追加できなくなった こと - LINE Developers
「LIFF APIリファレンス」 :liff.getIDToken()(IDトークンは発行から 1時間)、 liff.getAccessToken()(12時間、権限の 許可で 無効に なる こと)、 liff.permission.query()とliff.permission.requestAll()、liff.permanentLink.createUrlBy() - LINE Developers
「IDトークンから :IDトークンのプロフィール情報を 取得する」 中身と 検証 - LINE Developers
「LINEログイン v2.1 APIリファレンス」 :IDトークンを検証する エンドポイント、 パラメータ、 レスポンス、 エラー - LINE Developers
「チャネルアクセストークン」 :ステートレスと短期の トークン - LINE Developers
「Messaging APIリファレンス」 :ステートレスチャネルアクセストークンの発行
よくある質問
LINEミニアプリとLIFFアプリは、何が違いますか?
どちらも
Messaging APIのチャネルに、新しくLIFFアプリを追加できますか?
できません。
審査に通る前でも、お客様に使ってもらえますか?
チャネルを
予約の確認や前日の連絡を、サービスメッセージで送る条件は何ですか?
認証済ミニアプリである
手元のパソコンで作りながら、スマホのLINEで確かめるにはどうしますか?
LIFF CLIの
