※本記事はプロモーションを含みます
目次
Google Apps Script(GAS)を使って日々の業務を自動化していると、スプレッドシート内で閉じた集計だけでなく、外部のWebサービスやSaaSに蓄積データを連携したい局面が必ず訪れます。なぜなら、問い合わせフォームから送信されたリード情報をCRMに登録したり、売上確定レコードを決済システムへ通知したり、HubSpotなどの外部ツールにイベントをプッシュしたりする処理が必要になるからです。本稿では、GASで外部APIにPOSTリクエストでJSONを送信する際に必須となる実装と、業務運用で破綻させないための設計事項を整理します。
スプレッドシートの更新を外部SaaSへ即座に通知・連携したい場面
スプレッドシートは手軽に入力できるデータベース代わりとして広く使われていますが、複数のチームやシステムが関わる業務では、スプレッドシートに書き込まれた内容を外部システムへリアルタイムに連携する必要があります。たとえば、営業担当者がスプレッドシートに行を追加した瞬間にHubSpotへリード情報を作成するケースがあります。なので、ステータスが「承認」に変わったトリガーで決済代行サービスのAPIを叩いて請求を発行するケースなども代表的です。
こうした外部SaaS連携では、GETリクエストによる情報の取得ではなく、POSTリクエストを用いてこちらのデータを相手先のサーバーに作成・更新させることが基本動作となります。というのは、現代の多くのWeb APIは、送信するデータフォーマットとしてJSON(JavaScript Object Notation)を指定しているからです。GASから外部APIへJSONデータをPOST送信する場合、単純にオブジェクトを渡すだけでは相手側に意図した形式で届きません。ただし、HTTPリクエストのヘッダーでどのような形式のデータを送っているのかを明示し、オブジェクト自体も相手が受け取れる文字列形式に正しく変換する必要があります。
この通信要件を無視してリクエストを投げると、API側から400 Bad Request(不正なリクエスト)や415 Unsupported Media Type(サポートされていないメディアタイプ)といったエラーが返されます。なので、データ連携が失敗します。まずはプログラムの標準機能であるUrlFetchAppツールを使い、最小構成で正しくJSONをPOST送信するベースの処理を確認しましょう。
UrlFetchAppでJSON形式のPOSTリクエストを正しく組み立てる
GASで外部APIとHTTP通信を行う際は、組み込みのUrlFetchAppクラスのfetchメソッドを利用します。ただし、POSTリクエストでJSONデータを送信する場合、送信先URLに加えて第2引数のoptionsオブジェクトにHTTPメソッド、Content-Typeヘッダー、そしてシリアライズしたペイロードを設定します。
function sendLeadToExternalApi() { ... }
この処理で中核となるのが、optionsオブジェクト内のcontentTypeとJSON.stringifyです。というのは、GASのUrlFetchAppは、contentTypeを指定しない場合、デフォルトで「application/x-www-form-urlencoded」としてペイロードを扱おうとするからです。なので、明示的に「application/json」を指定しなければ、受信側のAPIサーバーはボディに送られてきた文字列をJSONとして認識できません。
また、payloadプロパティにJavaScriptの連想配列(オブジェクト)をそのまま渡してしまうと、GAS内部の挙動によってキーと値がフォームデータ形式に強制変換されたり、意図しない文字列化が行われて通信に失敗したりします。ただし、必ずJSON.stringify(payload)を実行し、純粋なJSON文字列へシリアライズした状態で渡す必要があります。
この実装でよく生じるズレが、日付オブジェクト(Date型)やスプレッドシートの空セル(undefined)の扱いです。なぜなら、スプレッドシートのセルからgetValuesで取得した値をそのままpayloadの連想配列に組み込むと、Date型はISOフォーマット文字列(例: 2024-01-01T00:00:00.000Z)へ変換されるからです。ただし、受信側のAPIが「YYYY-MM-DD」形式の文字列やUnixタイムスタンプしか受け付けない仕様だった場合、バリデーションエラーで弾かれます。なので、値がundefinedであるプロパティはJSON.stringifyによってキーごと丸ごと削除されるため、API側で「必須フィールドが存在しない」とみなされることがあります。外部APIへPOST送信する前には、API仕様書で要求されているデータ型に合わせて、スプレッドシートから取得した値を整型するステップを挟むことが安全策となります。
レスポンス判定と例外処理を組み込んでサイレントエラーを防ぐ
上記のコードは通信が完全に成功(HTTPステータス200番台)することを前提としています。ただし、実務で運用するスクリプトでは外部APIの障害、入力データのバリデーション違反、認証期限切れなどによって400番台や500番台のエラーが頻繁に発生します。
UrlFetchApp.fetchは、標準の動作ではHTTPステータスが4xxや5xxなどのエラーを返した際に、スクリプト実行そのものを例外(Exception)として即座に停止させます。なぜなら、スプレッドシート上で数百行のデータをループ処理している場合、途中の1件でAPIが400エラーを返しただけでスクリプト全体がクラッシュし、それ以降の行の処理がすべて中断してしまうからです。なので、GASが画面上に表示するエラーメッセージは「リクエストが失敗しました(400)」といった大雑把な内容になりがちで、API側がJSONボディ内で返してきた具体的なエラー詳細(どのフィールドが不足しているのか等)を確認できなくなります。
この問題を回避するためには、optionsに「muteHttpExceptions: true」を設定し、レスポンスコードに応じた分岐処理を明示的に記述しましょう。
function sendPostWithHandling() { ... }
muteHttpExceptionsをtrueに設定することで、HTTPエラーが発生してもUrlFetchAppは例外を投げず、通常通りHTTPResponseオブジェクトを返します。なので、getResponseCode()でステータスコードを判定し、200番台であれば成功、それ以外であればgetContentText()からAPI固有のエラー理由を抽出してスプレッドシートのログ列に書き出す、といったきめ細かいエラー処理が可能になります。
ただし、指定したURLのドメインが存在しない場合や、GASのネットワーク接続自体がタイムアウトした場合には、muteHttpExceptionsを指定していてもJavaScriptの例外が発生します。というのは、UrlFetchApp.fetchの呼び出し自体は必ずtry-catch文で囲み、通信環境そのものの異常にも耐えられるようにしておくのが実務上の定石となるからです。
外部API特有の認証ヘッダーやレートリミットに備える設計
業務用の外部サービスとAPI連携を行う場合、URLにPOSTリクエストを送るだけで完結することは稀です。なぜなら、ほとんどのSaaSでは認証が要求され、リクエストヘッダーにBearerトークンや独自のAPIキーを付与する必要があるからです。
UrlFetchAppでカスタムヘッダーを付与するには、optionsの「headers」プロパティに連想配列を指定します。たとえば、Bearer認証を行う場合は「Authorization: ‘Bearer
function sendAuthenticatedRequest() { ... }
外部APIを叩く際に直面するもう一つの壁がレートリミット(Rate Limit)です。というのは、多くのSaaSは「1分間に最大100リクエストまで」「秒間5リクエストまで」といった通信制限を設けているからです。スプレッドシートの行数が多い場合、forループで次々にUrlFetchApp.fetchを呼ぶと、あっという間に制限に達して「429 Too Many Requests」エラーが返ってきます。
これを回避するためには、リクエストとリクエストの間に「Utilities.sleep(ミリ秒)」を挟んで意図的に待機時間を設けましょう。ただし、外部APIがバルク作成エンドポイント(複数レコードを配列でまとめてPOSTできる仕様)を提供しているなら、1件ずつ送るのではなく配列に束ねて1回のリクエストで送信する形に設計を切り替える必要があります。
一括送信時にGoogle Apps Scriptの実行制限や通信オーバーヘッドを抑える工夫
Google Apps Scriptには、1回の実行時間の上限(通常アカウントで6分、Google Workspaceアカウントで30分)と、UrlFetchAppの一日あたりの呼び出し回数制限(通常アカウントで20,000回、Workspaceで100,000回)が存在します。なぜなら、スプレッドシートに蓄積された数千件のレコードを外部APIへPOST送信しようとすると、ネットワーク通信の往復待ち時間(レイテンシ)が積み重なり、6分の上限に引っかかってスクリプトが強制終了する事故が多発するからです。
たとえば、1回のリクエストに平均300ミリ秒かかると仮定すると、1,000件のデータを1件ずつ直列でPOST送信するだけで約300秒(5分)を消費します。なので、途中でわずかに通信が遅延しただけで、6分の壁を超えてしまう計算になります。
大量のPOST通信をGASから行う場合は、UrlFetchApp.fetchAllメソッドの活用を検討しましょう。というのは、fetchAllは、複数のリクエスト設定を配列として渡すことで、GASの内部で並列(または高速なパイプライン処理)通信を実行してくれるメソッドだからです。
function sendBatchPostRequests() { ... }
UrlFetchApp.fetchAllを利用すると、1件ずつ順次フェッチするループ構造に比べて全体の所要時間を大幅に短縮できます。ただし、並列で一度に数百件以上のリクエストを送信すると、相手先サーバーのファイアウォールやWAFによってDDoS攻撃と判定され、IPブロックや一斉429エラーを受けるリスクがあります。
そのため、実務では配列を20件〜50件程度のチャンク(小さな塊)に分割し、チャンクごとにfetchAllを実行して、チャンク間にUtilities.sleepで1秒ほどの待機を挟むといったバッチ制御が推奨されます。なので、スプレッドシートの端の列に「送信完了フラグ」や「APIレスポンスID」を記録しておき、万が一途中でタイムアウトや通信エラーが起きても、次回実行時に未送信行だけをフィルタリングして再開できる冪等(べきとう)性を持たせた構造にしておきましょう。

