MENU

GAS×Webhook入門|外部サービスと連携する方法

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()

次のステップ


この記事は SkillUp Labs が執筆しました。質問があればお気軽にお問い合わせください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次