はじめに
前回記事(リンク)で個人宛てのPayPay通知スクリプトはできたのですが、グループにするためにはグループIDを取得する必要があり、その取得方法が思ったより難解でした。いろいろ試行錯誤した結果、なんとか動くものができたので記事にしています。
基本的な設定方法(LINE Developersアカウント作成、Messaging APIチャネル設定など)は前回記事に書いていますので、ここでは割愛しています。
PayPayが強力なAPIを持っていればすぐに解決できたのですが…あるんでしょうか?結局Gmail経由で通知を拾う方法になりました。
必要なもの
- LINE Developersアカウント(Messaging APIチャネル)
- Google Apps Script
- 通知を送りたいLINEグループ
全体の流れ
- LINE Developersの設定変更
- グループID取得用のWebhookスクリプト作成・デプロイ
- Botをグループに追加
- グループIDの確認
- PayPay通知スクリプトの実装
最大のハマりポイント:Webhook検証エラーは無視してOK
まず最初に、私がハマった一番のポイントを書いておきます。LINE DevelopersでWebhook URLを設定する際、「検証」ボタンを押すと以下のエラーが表示されますが、これは完全に無視してOKでした。

ボットサーバーから200以外のHTTPステータスコードが返されました。(302 Found)
LINEプラットフォームから送信されたHTTP POSTリクエストに対してボットサーバーがステータスコード200を返すことを確認してください。
このエラーはGoogle Apps ScriptがLINEからの検証リクエストに対してリダイレクトレスポンスを返すことが原因ですが、実際の動作には全く影響しません。検証エラーが出ても、実際にBotは正常に動作します。
ステップ1:LINE Developersの設定
LINE Developersコンソールで以下の設定を行います:
- Messaging APIの有効化
- Webhook URLの設定(後でGoogle Apps ScriptのURLを入力)
- グループトークへの参加を許可する設定に変更

この設定変更を忘れると、Botをグループに招待してもはじかれてしまいます。
ステップ2:グループID取得用Webhookスクリプト
Google Apps Scriptで新しいプロジェクトを作成し、以下のコードを入力します:
// ============== 完全版Webhook(エラー対策済み) ==================
// GETリクエスト用(必須)
function doGet(e) {
return ContentService.createTextOutput('LINE Webhook is ready');
}
// POSTリクエスト用
function doPost(e) {
try {
// ログ記録
console.log('doPost called:', new Date().toISOString());
// eが存在しない場合の対策
if (!e) {
console.log('No event object');
return ContentService.createTextOutput('No event');
}
// postDataが存在しない場合の対策
if (!e.postData) {
console.log('No postData');
return ContentService.createTextOutput('No postData');
}
// contentsを取得
const contents = e.postData.contents;
if (!contents) {
console.log('No contents');
return ContentService.createTextOutput('No contents');
}
// JSONパース
let json;
try {
json = JSON.parse(contents);
} catch (parseError) {
console.error('JSON parse error:', parseError.toString());
return ContentService.createTextOutput('Parse error');
}
// イベント処理
if (json.events && Array.isArray(json.events)) {
json.events.forEach((event, index) => {
console.log(`Processing event ${index}: ${event.type}`);
// グループIDの処理
if (event.source && event.source.groupId) {
const groupId = event.source.groupId;
console.log('Group ID found:', groupId);
// グループIDを保存
saveGroupIdSafely(groupId);
}
});
}
// 成功レスポンス
return ContentService.createTextOutput('OK');
} catch (error) {
// エラーをログに記録
console.error('doPost error:', error.toString());
console.error('Stack:', error.stack);
// エラーを保存
try {
PropertiesService.getScriptProperties()
.setProperty('LAST_ERROR', error.toString());
} catch (e) {
// PropertiesServiceも失敗した場合
console.error('Cannot save error');
}
// エラーでも200 OKを返す
return ContentService.createTextOutput('Error occurred');
}
}
// 安全にグループIDを保存
function saveGroupIdSafely(groupId) {
try {
// テスト用IDは除外
if (groupId.includes('test') || groupId.includes('xxxx')) {
console.log('Skip test ID:', groupId);
return;
}
const props = PropertiesService.getScriptProperties();
// 最新のIDを保存
props.setProperty('LATEST_GROUP_ID', groupId);
// 履歴も保存
let history = props.getProperty('GROUP_ID_HISTORY');
try {
history = history ? JSON.parse(history) : [];
} catch (e) {
history = [];
}
if (!history.includes(groupId)) {
history.push(groupId);
// 最大10件まで保持
if (history.length > 10) {
history = history.slice(-10);
}
props.setProperty('GROUP_ID_HISTORY', JSON.stringify(history));
}
console.log('Group ID saved successfully:', groupId);
} catch (error) {
console.error('Save error:', error.toString());
}
}
// 保存されたグループIDを表示
function showSavedGroupId() {
const props = PropertiesService.getScriptProperties();
console.log('=== 保存されているグループID ===');
// 最新のID
const latestId = props.getProperty('LATEST_GROUP_ID');
if (latestId && !latestId.includes('test')) {
console.log('\n📋 最新のグループID:');
console.log(latestId);
console.log('\nPayPay通知スクリプト用:');
console.log(`const GROUP_ID = '${latestId}';`);
}
// 履歴
const history = props.getProperty('GROUP_ID_HISTORY');
if (history) {
try {
const ids = JSON.parse(history);
const realIds = ids.filter(id => !id.includes('test'));
if (realIds.length > 0) {
console.log('\n履歴:');
realIds.forEach((id, i) => console.log(`${i + 1}. ${id}`));
}
} catch (e) {
console.log('履歴の読み込みエラー');
}
}
// エラー確認
const lastError = props.getProperty('LAST_ERROR');
if (lastError) {
console.log('\n⚠️ 最後のエラー:');
console.log(lastError);
}
if (!latestId || latestId.includes('test')) {
console.log('\n実際のグループIDはまだ保存されていません');
console.log('LINEグループでBotにメッセージを送ってみてください');
}
}
// デバッグ情報
function showDebugInfo() {
console.log('=== デバッグ情報 ===');
const props = PropertiesService.getScriptProperties();
const allProps = props.getProperties();
console.log('\n保存されているすべてのプロパティ:');
for (const key in allProps) {
console.log(`${key}: ${allProps[key].substring(0, 50)}...`);
}
console.log('\n📌 次の手順:');
console.log('1. このコードを保存');
console.log('2. デプロイ → デプロイを管理 → 編集 → 新バージョン');
console.log('3. LINEグループでメッセージを送信');
console.log('4. showSavedGroupId()を実行');
}
ステップ3:デプロイとWebhook URL設定
- デプロイ:「デプロイ」→「新しいデプロイ」→「種類をウェブアプリ」→「アクセスできるユーザー:全員」
- URL取得:デプロイ完了後に表示されるURLをコピー
- LINE Developersに設定:Messaging APIタブの「Webhook URL」に貼り付け
⚠️ 重要:検証ボタンを押すと302エラーが出ますが、無視してください。実際の動作には問題ありません。
ステップ4:グループIDの取得
- Botをグループに追加
- グループ内でメッセージを送信(誰が送信してもOK)
- Google Apps Scriptで
showSavedGroupId()
を実行
グループIDが正常に取得できていることを確認します。
ログ上に以下のようなグループIDが出てくると思います。
情報 const GROUP_ID = ‘Ceda3e9ba35xxxxxxxxxxxxxx’;
ステップ5:PayPay通知スクリプトの実装
新しいGoogle Apps Scriptプロジェクトを作成し、以下のコードを実装します:
(前回から少し修正しています。)
以下の二つを設定する必要があります。
- CHANNEL_ACCESS_TOKEN
- const GROUP_ID
//============== 完成版:PayPayグループ通知スクリプト ==================
function sendPayPayNotificationToGroup() {
// ▼▼▼【重要】ここから2箇所を設定してください ▼▼▼
// 1. LINE Developersの「Messaging API」タブにある「チャネルアクセストークン」を貼り付け
const CHANNEL_ACCESS_TOKEN = 'YOUR_CHANNEL_ACCESS_TOKEN';
// 2. 取得したグループID
const GROUP_ID = 'YOUR_GROUP_ID';
// 3. 検索したいGmailの条件を指定
const SEARCH_QUERY = 'subject:("【PayPay】取引が完了しました") is:unread';
// ▲▲▲ 設定はここまでです ▲▲▲
try {
const threads = GmailApp.search(SEARCH_QUERY);
if (threads.length > 0) {
console.log(`${threads.length}件の新着PayPay通知を検出`);
threads.forEach(thread => {
const message = thread.getMessages()[0];
const subject = message.getSubject();
const body = message.getPlainBody();
const date = message.getDate();
// 金額を抽出
const amountRegex = /(\d[\d,]*)円/;
const match = body.match(amountRegex);
let amount = '金額不明';
if (match) {
amount = match[0];
}
// 日時をフォーマット
const formattedDate = Utilities.formatDate(date, 'Asia/Tokyo', 'MM/dd HH:mm');
// LINEに送るメッセージを組み立てる
const lineMessage = `💰 PayPay売上通知\n` +
`━━━━━━━━━━\n` +
`金額: ${amount}\n` +
`日時: ${formattedDate}\n` +
`━━━━━━━━━━`;
// グループに送信
const payload = {
'to': GROUP_ID, // グループIDを使用
'messages': [{
'type': 'text',
'text': lineMessage
}]
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': {
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
},
'payload': JSON.stringify(payload)
};
try {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', options);
console.log(`通知送信成功: ${amount}`);
} catch (sendError) {
console.error('送信エラー:', sendError.toString());
}
// メールを既読にする
thread.markRead();
});
console.log('すべての通知を送信完了');
} else {
console.log('新着のPayPay通知はありません');
}
} catch (e) {
console.error('エラーが発生しました:', e.toString());
}
}
// テスト送信用関数
function testGroupNotification() {
const CHANNEL_ACCESS_TOKEN = 'YOUR_CHANNEL_ACCESS_TOKEN';
const GROUP_ID = 'YOUR_GROUP_ID';
const testMessage = `🔔 テスト通知\n` +
`━━━━━━━━━━\n` +
`これはテスト送信です\n` +
`PayPay通知が正常に\n` +
`動作することを確認中\n` +
`━━━━━━━━━━\n` +
`時刻: ${new Date().toLocaleString('ja-JP')}`;
const payload = {
'to': GROUP_ID,
'messages': [{
'type': 'text',
'text': testMessage
}]
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': {
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
},
'payload': JSON.stringify(payload)
};
try {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', options);
console.log('テスト通知を送信しました!');
console.log('LINEグループを確認してください。');
} catch (e) {
console.error('送信エラー:', e.toString());
}
}
トリガー設定

「トリガーを追加」 設定:
- 実行する関数:
sendPayPayNotificationToGroup
- イベントのソース:時間主導型
- 時間ベースのトリガー:分ベースのタイマー
- 間隔:5分おきまたは10分おき

主なハマりポイントと解決策
1. Webhook検証(Verify)の302エラー問題
症状: LINE DevelopersでWebhook URLを検証すると「302 Found」エラー
原因: Google Apps ScriptがLINEからの検証リクエストに対してリダイレクトレスポンスを返す
解決策: Verifyボタンは無視して、実際の動作で確認する
2. doPost関数のエラーハンドリング
初期の問題: doPost関数がエラーで失敗し続けていた
原因:
- エラー時も必ず200 OKを返す必要がある
e.postData.contents
への安全なアクセスが必要- JSONパースエラーの適切な処理が必要
解決策: try-catchで確実にエラーをキャッチし、どんな場合でもContentService.createTextOutput()
を返す
3. グループ参加イベントの仕様
誤解: Botが既にグループにいる場合でもIDが取得できると思っていた
実際: joinイベントはBotがグループに追加された時のみ
解決策: 既存グループの場合は、一度退出させて再招待が必要。または、グループ内でメッセージを送信してもらう
4. USER_IDとGROUP_IDの違い
当初の課題: 個人のUSER_ID取得が困難
解決: グループIDなら簡単に取得でき、複数人への通知も可能
5. デプロイバージョンの更新忘れ
症状: コードを修正してもWebhookが古い動作のまま
原因: デプロイ時に「新バージョン」を選択し忘れ
解決: デプロイ管理で必ず新バージョンとして更新
トラブルシューティング
ログの確認方法
Google Apps Scriptの「実行」タブでログを確認できます。console.log()
で出力した内容がここに表示されます。
また、左側の「実行数」を確認するのも手の一つです。エラーが発生していると実行数が止まったり、エラーアイコンが表示されたりするので、定期実行が正常に動いているかチェックできます。

よくあるエラーと対処法
- 「グループIDが取得できない」
- Botがグループに正しく追加されているか確認
- グループ内でメッセージを送信してみる
showSavedGroupId()
でログを確認
- 「通知が送信されない」
- チャネルアクセストークンが正しいか確認
- グループIDが正しいか確認
testGroupNotification()
でテスト送信を試す
- 「Webhook URLが動作しない」
- デプロイ時に「アクセスできるユーザー:全員」を選択
- 新しいバージョンでデプロイされているか確認
まとめ
LINE NotifyからMessaging APIへの移行は、思ったより複雑でしたが、一度設定すればより柔軟な通知システムを作ることができました。特にグループ通知は複数人での情報共有にとても便利です。
一番のポイントは「Webhook検証エラーは無視してOK」ということでした。このエラーに惑わされず、実際の動作確認で進めることが重要だと分かりました。
同じような問題で困っている方の参考になれば幸いです。ぜひ試してみてください!