Apps Script example · 7 min read

Receive a Webhook with doPost and Log It to a Sheet: Copy-Paste Apps Script Pattern

Working receive a webhook with dopost and log it to a sheet example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

Web appdoPostWebhooks

Turning an Apps Script project into a webhook receiver is a surprisingly quick way to get third-party events like a payment notification or a form submission from another system straight into a spreadsheet without standing up a server.

This script implements a doPost function that Apps Script automatically calls whenever the deployed web app URL receives a POST request, checking a shared secret in the payload before trusting anything else in the body.

After verifying the secret, the function parses the JSON body, writes selected fields into a sheet as a new row, and responds with a small JSON object so the calling system can confirm the delivery succeeded.

Because webhook senders typically expect a fast, predictable response, the function is kept intentionally short, doing just enough validation and logging before returning rather than performing slow follow-up work inline.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

ResourceName / valuePurpose
Web app deployExecute as me / AnyonePublic doPost endpoint
Script propertyWEBHOOK_SECRETShared secret check
SheetWebhook LogappendRow of payload fields

What It Does

The doPost function receives the raw POST event, parses e.postData.contents as JSON, and immediately checks a secret field in that payload against a value stored in Script Properties before doing anything else with the data.

Once the secret check passes, the function extracts the expected fields from the parsed payload, appends them as a new row to a logging sheet, and returns a ContentService JSON response indicating success along with the new row number.

Prerequisites

Deploy the script as a web app under Deploy, New deployment, choosing Web app as the type and setting access to at least the senders you expect, then copy the resulting URL for the sending system to use.

Store a shared secret string in Script Properties under a key like WEBHOOK_SECRET, and share that same value with whoever configures the sending system so both sides agree on what proves a request is legitimate.

Walkthrough

Paste the doPost function into the script editor, set EXPECTED_FIELDS to the JSON keys you want pulled out of each payload, and deploy the project as a web app, copying the deployment URL.

Send a test POST request with curl containing a JSON body with the correct secret and expected fields, then check the target sheet to confirm a new row appeared with the right values.

Send a second test request with an incorrect secret and confirm the response comes back as an authorization failure rather than a row being appended to the sheet.

Edge Cases

A POST request with a body that is not valid JSON is caught by a try/catch around JSON.parse, and the function responds with a 400-style JSON error rather than throwing an unhandled exception that would appear as a generic server error to the sender.

Redeploying the web app after making a code change creates a new deployment URL unless you explicitly deploy a new version of an existing deployment, so senders configured with the old URL would silently stop receiving updates until you correct the URL on their end.

Testing

Use curl or a tool like webhook.site's request replay feature to send a payload matching your EXPECTED_FIELDS and confirm the appended row's values line up exactly with what you sent.

Send a payload missing one of the EXPECTED_FIELDS entirely and confirm the corresponding sheet cell comes through blank rather than the whole request being rejected.

Hardening

Compare the secret using a constant-time-ish approach or at minimum keep it out of logs entirely, since logging the full incoming payload for debugging could otherwise leak the shared secret into your Apps Script execution logs.

Rate-limit or deduplicate incoming requests using a hash of the payload stored temporarily in CacheService, since some webhook senders retry deliveries and you may not want duplicate rows for the same logical event.

Variations

Chain the received webhook data into the send-webhook tutorial's UrlFetchApp pattern to forward or acknowledge the event to a second downstream system after logging it.

Return different HTTP-style status information in the ContentService response body based on validation results, which lets more sophisticated senders distinguish a secret failure from a malformed payload.

doPost.gs

doPost parses the incoming JSON body defensively, checks a shared secret against Script Properties, appends the event to a log sheet, and replies with a JSON acknowledgement.

// Receive, verify, and log an incoming webhook POST request
function doPost(e) {
  var SHEET_NAME = 'Webhook Log';
  var EXPECTED_FIELDS = ['eventType', 'orderId', 'amount'];
  var storedSecret = PropertiesService.getScriptProperties().getProperty('WEBHOOK_SECRET');

  var payload;
  try {
    payload = JSON.parse(e.postData.contents);
  } catch (err) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: 'invalid_json' }))
      .setMimeType(ContentService.MimeType.JSON);
  }

  if (payload.secret !== storedSecret) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: 'unauthorized' }))
      .setMimeType(ContentService.MimeType.JSON);
  }

  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  var row = [new Date()];
  for (var i = 0; i < EXPECTED_FIELDS.length; i++) {
    row.push(payload[EXPECTED_FIELDS[i]] || '');
  }
  sheet.appendRow(row);

  return ContentService.createTextOutput(JSON.stringify({ ok: true, row: sheet.getLastRow() }))
    .setMimeType(ContentService.MimeType.JSON);
}
  1. Line 5: Reading the secret from Script Properties instead of hardcoding it means rotating the secret never requires touching the code.
  2. Line 9: Wrapping JSON.parse in try/catch means a malformed body produces a clean JSON error response instead of an unhandled exception.
  3. Line 15: Comparing the payload's own secret field against the stored value happens before any data is trusted or written anywhere.
  4. Line 23: Falling back to an empty string for a missing expected field keeps appendRow working even when a sender omits an optional key.
  5. Line 27: Returning the new row number in the success response gives the sender a concrete acknowledgment that the event was actually stored.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: webhook receive doPost

  • 1Script deployed as a web app with appropriate access level
  • 2Shared secret stored in Script Properties under WEBHOOK_SECRET
  • 3EXPECTED_FIELDS set to match the sending system's payload keys
  • 4Logging sheet created with matching columns
  • 5Test POST request sent with curl and a correct secret
  • 6Second test sent with an incorrect secret to confirm rejection
  • 7Invalid JSON body tested to confirm graceful error response

Frequently asked questions

A try/catch around JSON.parse catches that case and returns a JSON error response instead of throwing an unhandled exception.

Only if you create a brand-new deployment; deploying a new version of an existing deployment keeps the same URL, which senders should be configured to use.

The parsed payload's secret field is compared against a value stored in Script Properties before any other processing happens.

Yes, chain the received webhook data into the send-webhook tutorial's UrlFetchApp pattern to forward or acknowledge the event downstream.

Deduplicate using a hash of the payload stored temporarily in CacheService, as suggested in the hardening section.

It should not be, since the hardening guidance explicitly warns against logging the full incoming payload for debugging.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.