Amazonで商品を見るセール会場へ

Googleスプレッドシート│POST通信で書き込む方法

GASのdoPostでJSONを受け取り、指定したシートに記録します。

この例は練習用の記録先を明示し、入力検査とリクエストIDによる再追記防止を行います。

練習コード・入力JSON・文字変換ブックをダウンロード(ZIP)できます。

記事ごとに別のApps Scriptプロジェクトを使い、新規の練習データを対象にしてください。

最終確認は2026年10月2日です。

GASの51項目はローカルの模擬検証、curlの3項目はローカルHTTP環境、数式の8項目はArtifact Toolでの検証です。

Googleへの認可・実シートの操作・カレンダーの列挙・配備後のHTTP通信・Google Sheetsでの数式計算は未検証です。

目次

練習シートとスクリプトプロパティ

新規の練習ブックに「practice-log」シートを作り、A1:D1にrequest_id、script、name、dateの順で見出しを入れます。

シートの共有範囲は広げず、実行するアカウントから編集できることを確認します。

プロパティ設定値
SPREADSHEET_ID練習ブックのID
SHEET_NAMEpractice-log
POST_TOKEN自分で用意した練習専用の長いランダムな文字列。公開記事や共有ファイルには載せない

「拡張機能」→「Apps Script」を開き、プロジェクトの設定からスクリプトプロパティを指定します。

以下のコードを保存します。

doGetは動作確認用の応答だけを返し、シートの行やトークンを公開しません。

// この例は、新規の練習シートだけで使います。
function fail_(code) {
  const error = new Error(code);
  error.publicCode = code;
  throw error;
}
function property_(key) {
  const value = PropertiesService.getScriptProperties().getProperty(key);
  if (!value) fail_('missing_configuration');
  return value;
}
function json_(value) {
  return ContentService.createTextOutput(JSON.stringify(value))
    .setMimeType(ContentService.MimeType.JSON);
}
function payload_(event) {
  if (!event || !event.postData || typeof event.postData.contents !== 'string') fail_('missing_body');
  if (!/^application\/json(?:\s*;|$)/i.test(event.postData.type || '')) fail_('content_type');
  const text = event.postData.contents;
  if (!text || text.length > 4096) fail_('body_size');
  let data;
  try { data = JSON.parse(text); } catch (_) { fail_('invalid_json'); }
  const keys = ['token', 'request_id', 'script', 'name', 'date'];
  if (!data || Array.isArray(data) || typeof data !== 'object' ||
      Object.keys(data).length !== keys.length || !keys.every(k => typeof data[k] === 'string')) fail_('schema');
  if (data.token !== property_('POST_TOKEN')) fail_('unauthorized');
  if (!/^[a-zA-Z0-9_-]{8,80}$/.test(data.request_id)) fail_('request_id');
  for (const key of ['script', 'name']) {
    data[key] = data[key].trim();
    if (!data[key] || data[key].length > 120 || /^[=+@-]/.test(data[key]) || /[\r\n\t]/.test(data[key])) fail_('unsafe_text');
  }
  if (!/^\d{4}-\d{2}-\d{2}$/.test(data.date)) fail_('date');
  const date = new Date(data.date + 'T00:00:00Z');
  if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== data.date) fail_('date');
  return {request_id: data.request_id, script: data.script, name: data.name, date: data.date};
}
function logSheet_() {
  const sheet = SpreadsheetApp.openById(property_('SPREADSHEET_ID')).getSheetByName(property_('SHEET_NAME'));
  if (!sheet) fail_('missing_sheet');
  const expected = ['request_id', 'script', 'name', 'date'];
  const headers = sheet.getRange(1, 1, 1, 4).getValues()[0];
  if (!expected.every((x, i) => headers[i] === x)) fail_('headers');
  return sheet;
}
function appendOnce_(data) {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) fail_('busy');
  try {
    const sheet = logSheet_();
    const last = sheet.getLastRow();
    if (last > 1001) fail_('practice_limit');
    const rows = last > 1 ? sheet.getRange(2, 1, last - 1, 4).getValues() : [];
    const record = [data.request_id, data.script, data.name, data.date];
    const matches = rows.filter(row => row[0] === data.request_id);
    if (matches.length) {
      if (matches.length !== 1 || !record.every((x, i) => matches[0][i] === x)) fail_('request_id_conflict');
      return {ok: true, status: 'already_recorded', request_id: data.request_id};
    }
    if (last >= 1001) fail_('practice_limit');
    sheet.getRange(last + 1, 1, 1, 4).setNumberFormat('@').setValues([record]);
    SpreadsheetApp.flush();
    return {ok: true, status: 'recorded', request_id: data.request_id};
  } finally { lock.releaseLock(); }
}
function doPost(event) {
  try { return json_(appendOnce_(payload_(event))); }
  catch (error) { return json_({ok: false, error: error.publicCode || 'service_error'}); }
}
function doGet() {
  // トークンやシートの行はGETで返しません。
  return json_({ok: true, service: 'practice-log', read_enabled: false});
}
function demoPost() {
  const data = {token: property_('POST_TOKEN'), request_id: 'practice-20261002-01',
    script: 'demo.jsx', name: 'practice-user', date: '2026-10-02'};
  const response = doPost({postData: {type: 'application/json', contents: JSON.stringify(data)}});
  console.log(response.getContent());
}

エディタで先に1件を記録する

demoPostを選んで実行します。

練習ブックへのアクセス権限を確認して認可すると、request_idがpractice-20261002-01の行を記録します。

再実行時はalready_recordedを返し、同じ内容を増やしません。

失敗した場合はログのerrorと見出し・設定値を照合します。

curlから送る入力とリダイレクト

HTTPで試す場合は、「デプロイ」→「新しいデプロイ」でウェブアプリを選び、実行するアカウントとアクセスできるユーザーを確認します。

通常のcurlはブラウザーのGoogleログインを引き継ぎません。

サインイン必須の配備ではログイン画面になる場合があり、このコマンドだけでは認証できません。

匿名アクセスが必要な練習でも、共有トークンは必須です。

本番データを含むブックを匿名公開するための設計ではありません。

アクセス条件が合わない場合は設定を広げず、先のエディタ実行までで確認してください。

HTTPによる練習が終わった配備は管理画面から停止します。

request.jsonはUTF-8で保存します。

tokenは自分のPOST_TOKENへ置き換え、実際のトークンを公開したり、URLの引数に入れたりしないでください。

日付はYYYY-MM-DD形式です。

{
  "token": "REPLACE_WITH_YOUR_PRACTICE_TOKEN",
  "request_id": "practice-20261002-01",
  "script": "demo.jsx",
  "name": "practice-user",
  "date": "2026-10-02"
}

Windowsではcurl.exe –versionで利用できるか確認し、request.jsonと同じフォルダで次のコマンドを実行します。

YOUR_PRACTICE_EXEC_URLを、自分の練習用配備の /exec URLへ置き換えます。

macOS・Linuxではcurl.exeをcurlに読み替えます。

curl.exe --location --header "Content-Type: application/json" --data-binary "@request.json" "YOUR_PRACTICE_EXEC_URL"

Content Serviceの応答は別のURLへリダイレクトされるため、–locationを指定します。

–data-binaryで最初のPOSTを指定し、–request POSTは併用しません。

ローカルの302・303応答では、最初のPOSTの後にGETで応答を受け取ることを確認しました。

Googleへの実通信は未検証です。

確認結果と切り分け

条件結果
最初の正しい入力ok:true、status:recorded。データが1行増える
同じIDと同じ内容already_recorded。行は増えない
同じIDで異なる内容request_id_conflict。書き込まない
本文なし・不正JSON・項目違いmissing_body、invalid_json、schema
トークン違いunauthorized。書き込まない
数式のような文字・改行・長すぎる値unsafe_text。練習例の入力条件外として拒否
GETの応答接続確認のみ。シート設定や書き込みの成功は示さない
HTTP200でもok:false失敗。応答本文のerrorを確認する
1000件を超える練習データpractice_limit。部分的な走査で続行しない

LockServiceで同じプロジェクトの同時実行を制御します。

別プロジェクトや手動編集までは防げないため、練習中は他の処理から同じシートを変更しないでください。

通信が途中で切れた場合は、シートのrequest_idを確認してから同じIDで再確認します。

IDを変えると別件として記録されます。

停止と戻し方

この例は定期トリガーを作りません。

HTTPの受付を止めるときは配備を停止し、練習トークンの使用も終了します。

データを戻す必要がある場合は、変更前のコピーと記録したrequest_idを照合します。

本番の行を一括削除する操作は含めていません。

公式資料

ウェブアプリのイベントと配備、Content Serviceのリダイレクト、curlのリダイレクトとリクエスト方式を参照してください。

関連記事・旧実装の参考資料

旧GitHubコードや外部の参考例は、現在の記事のZIPと異なる場合があります。

今回の練習は本文のセル番地・プロパティ・ZIPをそろえて実行してください。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

スプレッドシート・GASのおすすめ書籍

作業環境の作り方

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