GAS×Webhook入門|外部サービスと連携する方法
Webhookを使えば、GASと外部サービスを簡単に連携できます。Slack通知、Discord投稿、各種サービスとの自動連携など、可能性は無限大です。
目次
Webhookとは何か
Webhookの基本概念
| 項目 | 内容 |
|---|---|
| 定義 | イベント発生時に自動でHTTPリクエストを送る仕組み |
| 別名 | HTTPコールバック、Webコールバック |
| 方向 | サービスA → サービスB(プッシュ型) |
| データ形式 | JSON(多くの場合) |
APIとの違い
| 項目 | Webhook | API |
|---|---|---|
| 通信方向 | サービス側から通知(プッシュ) | こちらから問い合わせ(プル) |
| タイミング | イベント発生時に即時 | 定期的にポーリング |
| 効率 | 高(必要な時だけ通信) | 低(無駄な通信が発生) |
| 用途 | リアルタイム通知 | データ取得・操作 |
Webhookの使用例
【送信側(Outgoing Webhook)】
GAS → Slack: スプレッドシート更新時にSlackに通知
GAS → Discord: フォーム回答時にDiscordに投稿
【受信側(Incoming Webhook)】
GitHub → GAS: プルリクエスト作成時にGASで処理
Stripe → GAS: 決済完了時にGASで記録
GASでWebhookを送信する(Outgoing)
基本: UrlFetchAppの使い方
GASから外部にWebhookを送信するには、UrlFetchAppを使います。
/**
* Webhook送信の基本形
*/
function sendWebhookBasic() {
const webhookUrl = 'https://example.com/webhook';
const payload = {
message: 'Hello from GAS!',
timestamp: new Date().toISOString()
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
const response = UrlFetchApp.fetch(webhookUrl, options);
console.log(response.getContentText());
}
実践例① Slack Webhook連携
SlackのIncoming Webhooksを使って、GASからSlackにメッセージを送信します。
STEP 1: Slack Webhook URLの取得
- Slack APIにアクセス
- 「Create New App」→「From scratch」
- アプリ名を入力し、ワークスペースを選択
- 「Incoming Webhooks」を有効化
- 「Add New Webhook to Workspace」でチャンネルを選択
- Webhook URLをコピー
STEP 2: GASコード
/**
* Slackにメッセージを送信する
* @param {string} message - 送信するメッセージ
* @param {string} channel - チャンネル名(オプション)
*/
function sendToSlack(message, channel) {
// ★ 自分のWebhook URLに置き換え
const webhookUrl = 'https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ';
const payload = {
text: message,
username: 'GAS Bot',
icon_emoji: ':robot_face:'
};
// チャンネル指定がある場合
if (channel) {
payload.channel = channel;
}
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
try {
UrlFetchApp.fetch(webhookUrl, options);
console.log('Slack送信成功');
} catch (error) {
console.error('Slack送信エラー:', error);
}
}
/**
* リッチなSlackメッセージを送信(Block Kit使用)
*/
function sendRichSlackMessage() {
const webhookUrl = 'https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ';
const payload = {
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: '📊 日次レポート'
}
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: '*売上:*\n¥1,234,567'
},
{
type: 'mrkdwn',
text: '*前日比:*\n+15.2%'
}
]
},
{
type: 'divider'
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `生成日時: ${new Date().toLocaleString('ja-JP')}`
}
]
}
]
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
UrlFetchApp.fetch(webhookUrl, options);
}
/**
* スプレッドシート更新時にSlack通知(トリガー用)
*/
function onSpreadsheetEdit(e) {
const sheet = e.source.getActiveSheet();
const range = e.range;
const value = e.value;
const message = `📝 スプレッドシート更新\n` +
`シート: ${sheet.getName()}\n` +
`セル: ${range.getA1Notation()}\n` +
`値: ${value}`;
sendToSlack(message);
}
トリガー設定
1. GASエディタで「トリガー」アイコンをクリック
2. 「トリガーを追加」
3. 関数: onSpreadsheetEdit
4. イベントの種類: 編集時
5. 保存
実践例② Discord Webhook連携
DiscordのWebhookを使って、GASからDiscordにメッセージを送信します。
STEP 1: Discord Webhook URLの取得
- Discordサーバーの「サーバー設定」
- 「連携サービス」→「ウェブフック」
- 「新しいウェブフック」を作成
- チャンネルを選択し、URLをコピー
STEP 2: GASコード
/**
* Discordにメッセージを送信する
* @param {string} message - 送信するメッセージ
*/
function sendToDiscord(message) {
// ★ 自分のWebhook URLに置き換え
const webhookUrl = 'https://discord.com/api/webhooks/XXXXX/YYYYY';
const payload = {
content: message,
username: 'GAS Bot'
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
try {
UrlFetchApp.fetch(webhookUrl, options);
console.log('Discord送信成功');
} catch (error) {
console.error('Discord送信エラー:', error);
}
}
/**
* Discordに埋め込みメッセージを送信(Embed使用)
*/
function sendDiscordEmbed() {
const webhookUrl = 'https://discord.com/api/webhooks/XXXXX/YYYYY';
const payload = {
username: 'GAS Bot',
embeds: [
{
title: '🔔 新着通知',
description: 'スプレッドシートに新しいデータが追加されました',
color: 5814783, // 青色(10進数)
fields: [
{
name: '項目',
value: 'サンプルデータ',
inline: true
},
{
name: 'ステータス',
value: '完了',
inline: true
}
],
footer: {
text: `送信日時: ${new Date().toLocaleString('ja-JP')}`
},
timestamp: new Date().toISOString()
}
]
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
UrlFetchApp.fetch(webhookUrl, options);
}
/**
* フォーム回答をDiscordに通知
*/
function onFormSubmitToDiscord(e) {
const responses = e.values;
const timestamp = responses[0];
let message = '📝 **新しいフォーム回答**\n';
message += `回答日時: ${timestamp}\n`;
message += '---\n';
// 回答内容を追加(2列目以降)
for (let i = 1; i < responses.length; i++) {
message += `${responses[i]}\n`;
}
sendToDiscord(message);
}
GASでWebhookを受け取る(Incoming)
doPost関数とdoGet関数
GASをWebアプリとしてデプロイすると、外部からのHTTPリクエストを受け取れます。
| 関数 | HTTPメソッド | 主な用途 |
|---|---|---|
doGet(e) |
GET | データ取得、ステータス確認 |
doPost(e) |
POST | データ受信、Webhook受付 |
基本構造
/**
* GETリクエストを処理
* @param {Object} e - イベントオブジェクト
* @return {TextOutput} レスポンス
*/
function doGet(e) {
// クエリパラメータを取得
const params = e.parameter;
return ContentService
.createTextOutput(JSON.stringify({ status: 'ok', params: params }))
.setMimeType(ContentService.MimeType.JSON);
}
/**
* POSTリクエストを処理
* @param {Object} e - イベントオブジェクト
* @return {TextOutput} レスポンス
*/
function doPost(e) {
// POSTデータを取得
const postData = JSON.parse(e.postData.contents);
// ここで処理を実行
console.log('受信データ:', postData);
return ContentService
.createTextOutput(JSON.stringify({ status: 'ok' }))
.setMimeType(ContentService.MimeType.JSON);
}
Webアプリとしてデプロイする手順
STEP 1: デプロイ設定
- GASエディタで「デプロイ」→「新しいデプロイ」
- 種類の選択でウェブアプリを選択
- 以下を設定:
| 項目 | 設定値 |
|---|---|
| 説明 | 任意(例: Webhook受信用) |
| 次のユーザーとして実行 | 自分 |
| アクセスできるユーザー | 全員 |
- デプロイをクリック
- WebアプリのURLをコピー(これがWebhook URL)
STEP 2: 更新時の注意
コードを変更した場合は、新しいデプロイが必要です。
「デプロイ」→「デプロイを管理」→「新しいバージョン」
※同じURLで更新する場合は「デプロイを編集」からバージョンを更新
実践例③ 外部サービスからの通知受信
GitHubのWebhookを受け取って、スプレッドシートに記録する例です。
/**
* GitHub Webhookを受け取ってスプレッドシートに記録
*/
function doPost(e) {
try {
// POSTデータを解析
const payload = JSON.parse(e.postData.contents);
// GitHubイベントの種類を取得
const eventType = e.parameter.event || 'unknown';
// スプレッドシートに記録
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Webhook履歴');
// 記録するデータを整形
const timestamp = new Date();
const action = payload.action || '';
const sender = payload.sender ? payload.sender.login : '';
const repository = payload.repository ? payload.repository.full_name : '';
// 行を追加
sheet.appendRow([
timestamp,
eventType,
action,
sender,
repository,
JSON.stringify(payload).substring(0, 500) // 最初の500文字
]);
// 成功レスポンス
return ContentService
.createTextOutput(JSON.stringify({ status: 'success' }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
console.error('Webhook処理エラー:', error);
// エラーレスポンス
return ContentService
.createTextOutput(JSON.stringify({ status: 'error', message: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
/**
* Webhook履歴シートを初期化(初回のみ実行)
*/
function initWebhookSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('Webhook履歴');
if (!sheet) {
sheet = ss.insertSheet('Webhook履歴');
}
// ヘッダー行を設定
sheet.getRange(1, 1, 1, 6).setValues([[
'受信日時', 'イベント種類', 'アクション', '送信者', 'リポジトリ', 'ペイロード(抜粋)'
]]);
// ヘッダー行を固定
sheet.setFrozenRows(1);
}
セキュリティ対策
なぜセキュリティが重要か
Webhookを受け取るURLは公開URLになります。悪意のあるリクエストを防ぐため、セキュリティ対策が必要です。
| リスク | 対策 |
|---|---|
| なりすまし | トークン検証 |
| 不正データ | 入力値検証 |
| リプレイ攻撃 | タイムスタンプ検証 |
| DoS攻撃 | レート制限 |
トークン検証の実装
/**
* トークン検証付きのWebhook受信
*/
function doPost(e) {
// 秘密トークン(スクリプトプロパティに保存推奨)
const SECRET_TOKEN = PropertiesService.getScriptProperties().getProperty('WEBHOOK_SECRET');
// リクエストヘッダーからトークンを取得
// ※ GASではヘッダーを直接取得できないため、クエリパラメータを使用
const requestToken = e.parameter.token;
// トークン検証
if (requestToken !== SECRET_TOKEN) {
return ContentService
.createTextOutput(JSON.stringify({ error: 'Unauthorized' }))
.setMimeType(ContentService.MimeType.JSON);
}
// 以降、正常な処理
const payload = JSON.parse(e.postData.contents);
// ... 処理を実行
return ContentService
.createTextOutput(JSON.stringify({ status: 'ok' }))
.setMimeType(ContentService.MimeType.JSON);
}
/**
* HMAC-SHA256署名検証(GitHub Webhooks用)
* ※ より厳密な検証が必要な場合
*/
function verifyGitHubSignature(payload, signature) {
const secret = PropertiesService.getScriptProperties().getProperty('GITHUB_SECRET');
const computedSignature = 'sha256=' +
Utilities.computeHmacSha256Signature(payload, secret)
.map(byte => ('0' + (byte & 0xFF).toString(16)).slice(-2))
.join('');
return computedSignature === signature;
}
スクリプトプロパティの設定
/**
* 秘密トークンをスクリプトプロパティに保存(初回のみ実行)
*/
function setWebhookSecret() {
const secret = 'your-secret-token-here'; // ★ 安全なランダム文字列を設定
PropertiesService.getScriptProperties().setProperty('WEBHOOK_SECRET', secret);
console.log('Secret設定完了');
}
入力値検証
/**
* 入力値を検証してから処理
*/
function processWebhookSafely(e) {
const payload = JSON.parse(e.postData.contents);
// 必須フィールドの確認
if (!payload.event || !payload.data) {
throw new Error('必須フィールドが不足しています');
}
// データ型の確認
if (typeof payload.data !== 'object') {
throw new Error('dataはオブジェクトである必要があります');
}
// 文字列長の制限
if (payload.message && payload.message.length > 1000) {
payload.message = payload.message.substring(0, 1000);
}
return payload;
}
よくあるエラーと対処法
エラー一覧
| エラー | 原因 | 対処法 |
|---|---|---|
Exception: Invalid argument |
URLが間違っている | Webhook URLを確認 |
Exception: Address unavailable |
サービスがダウン | 時間をおいて再試行 |
Script function not found: doPost |
関数名が違う | doPostを正確に記述 |
You do not have permission |
権限設定ミス | デプロイ時に「全員」を選択 |
| レスポンスが返らない | return忘れ |
必ずreturnを記述 |
デバッグ方法
/**
* Webhookのデバッグ用ログ記録
*/
function doPost(e) {
// 受信データをすべてログに記録
console.log('=== Webhook受信 ===');
console.log('postData:', e.postData);
console.log('parameter:', e.parameter);
console.log('contentLength:', e.contentLength);
// スプレッドシートにも記録(実行ログが見られない場合用)
const debugSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Debug');
if (debugSheet) {
debugSheet.appendRow([
new Date(),
JSON.stringify(e.postData),
JSON.stringify(e.parameter)
]);
}
return ContentService
.createTextOutput('OK')
.setMimeType(ContentService.MimeType.TEXT);
}
実践Tips
Slack/Discordへの通知を使い分ける
| 用途 | おすすめ | 理由 |
|---|---|---|
| チーム通知 | Slack | ビジネス利用に最適 |
| 個人・趣味 | Discord | 無料で制限が緩い |
| 緊急通知 | 両方 | 冗長化で確実に届く |
Webhook URL管理のベストプラクティス
/**
* Webhook URLを一元管理
*/
const WEBHOOK_URLS = {
slack: PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK'),
discord: PropertiesService.getScriptProperties().getProperty('DISCORD_WEBHOOK')
};
function notifyAll(message) {
sendToSlack(message);
sendToDiscord(message);
}
再試行ロジック
/**
* 失敗時に再試行するWebhook送信
*/
function sendWebhookWithRetry(url, payload, maxRetries = 3) {
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
for (let i = 0; i < maxRetries; i++) {
try {
const response = UrlFetchApp.fetch(url, options);
const code = response.getResponseCode();
if (code >= 200 && code < 300) {
return true; // 成功
}
console.log(`試行 ${i + 1} 失敗: HTTP ${code}`);
} catch (error) {
console.log(`試行 ${i + 1} エラー: ${error}`);
}
// 次の試行まで待機(指数バックオフ)
if (i < maxRetries - 1) {
Utilities.sleep(1000 * Math.pow(2, i));
}
}
return false; // 全試行失敗
}
まとめ
GAS×Webhookのポイント
| 項目 | 内容 |
|---|---|
| 送信(Outgoing) | UrlFetchApp.fetch()で外部に通知 |
| 受信(Incoming) | doPost()関数でデータを受け取り |
| デプロイ | Webアプリとして公開が必要 |
| セキュリティ | トークン検証を必ず実装 |
コード早見表
| 用途 | 使用する関数/メソッド |
|---|---|
| Webhook送信 | UrlFetchApp.fetch(url, options) |
| Webhook受信 | doPost(e), doGet(e) |
| JSONレスポンス | ContentService.createTextOutput() |
| 秘密情報管理 | PropertiesService.getScriptProperties() |
次のステップ
- GAS×ChatGPT連携|AIを組み込む方法[リンク予定]
- GAS×スプレッドシート|データ操作の基本[リンク予定]
- GAS×Gmail連携|メール自動化の完全ガイド[リンク予定]
この記事は SkillUp Labs が執筆しました。質問があればお気軽にお問い合わせください。
コメント