ProductsDocsBlogConsultingAboutContactGet Started
Back to BlogDiagram of external services like Stripe, Magento, and Twilio sending webhook requests through Apps Script doPost into a Google Sheet, with an HMAC signature verification step in between
9 min readMageSheet Team

Unleashing Apps Script Webhooks: Connect Any App to Google Sheets

Google WorkspaceApps ScriptWebhooksIntegrationsdoPostREST APIStripe

Hey everyone! Welcome back to MageSheet.

We've talked about how to completely overhaul your internal workflows by using Apps Script to build custom HTML interfaces and secure B2B client portals.

But what if you don't even want a user interface? What if you want your Magento store, your HubSpot CRM, or a custom mobile app to push data directly into your Google Sheets silently in the background?

To do this, we need to turn our Google Workspace environment into an API. And in Apps Script, that's done by building Webhooks.

What is a Webhook?

Think of an API as you walking up to a database and asking, "Hey, do you have any new orders?" (Polling). A Webhook, on the other hand, is the database saying, "Here's my phone number. Call me the millisecond a new order arrives."

Setting up a webhook listener in Google Apps Script is arguably the fastest, cheapest, and most reliable way to ingest live data from thousands of enterprise platforms like Magento, Shopify, Zapier, or Stripe without paying for third-party middleware like Make.com or Zapier.

Enter doPost() and doGet()

Apps Script has two magical, reserved functions that listen for incoming HTTP traffic:

  • doGet(e): Listens for incoming HTTP GET requests (usually data appended to a URL, like ?name=yusuf).
  • doPost(e): Listens for incoming HTTP POST requests (usually a hidden payload of JSON data).

If you define these functions and publish your script as a Web App, Google gives you a unique URL. Anytime an external system sends data to that URL, the corresponding function executes automatically!

Step 1: Writing the Webhook Listener

Let's say we have a Magento store and we want to push the customer's email and order total into a Google Sheet every time a successful checkout occurs.

Open Extensions > Apps Script in your blank Google Sheet and write this code:

// Code.gs

// The external system will send a JSON payload via HTTP POST
function doPost(e) {
  
  try {
    // 1. Parse the incoming JSON payload from the request body
    // e.postData.contents holds the raw JSON string sent by the webhook
    const payloadStr = e.postData.contents;
    const orderData = JSON.parse(payloadStr);
    
    // 2. Select the spreadsheet
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Orders");
    
    // 3. Prepare the row data
    const timestamp = new Date();
    const customerEmail = orderData.email || "No Email provided";
    const orderTotal = orderData.total || 0.00;
    const orderId = orderData.order_id || "Unknown";
    
    // 4. Quickly append to the sheet
    sheet.appendRow([timestamp, orderId, customerEmail, orderTotal]);
    
    // 5. Always return a 200 OK response so the sending app knows it succeeded
    return ContentService.createTextOutput(JSON.stringify({ 
      "status": "success", 
      "message": "Order appended!" 
    })).setMimeType(ContentService.MimeType.JSON);
    
  } catch (error) {
    // Handle failures gracefully
    return ContentService.createTextOutput(JSON.stringify({ 
      "status": "error", 
      "message": error.toString() 
    })).setMimeType(ContentService.MimeType.JSON);
  }
}

Step 2: Deploying the Webhook

Code alone isn't enough. We need the physical URL.

  1. Click Deploy > New deployment.
  2. Select type: Web app.
  3. Execute as: Me (Crucial!).
  4. Who has access: Anyone (So Magento, which is unauthenticated to Google, can reach it).
  5. Click Deploy.

You will receive a URL that looks like this: https://script.google.com/macros/s/AKfycby..._aBCD/exec

Copy this URL. This is your secure Webhook Endpoint.

Step 3: Sending Data (Testing)

You don't even need Magento to test this. You can use a tool like Postman, or a simple curl command in your terminal to fire data at your new Google Sheet API.

curl -X POST "https://script.google.com/macros/s/AKfycby..._aBCD/exec" \
     -H "Content-Type: application/json" \
     -d '{"email": "boss@company.com", "total": 450.00, "order_id": "MAG-9912"}'

If you look back at your Google Sheet, the data will instantly appear!

The Resulting Table in Sheets

TimestampOrderIDEmailTotal
10/27/2026 14:32:01MAG-9912boss@company.com$450.00

Securing Your Webhook with HMAC Signatures

The deployment we just walked through is wide open. Anyone with the URL can post arbitrary data to your Sheet. For prototypes that is fine; for anything production-bound, you need authentication.

The standard pattern is HMAC signature verification. The webhook sender computes a signature by hashing the request body with a shared secret, and includes the signature as a header. Your doPost recomputes the same hash and rejects the request if the signatures do not match.

const WEBHOOK_SECRET = PropertiesService.getScriptProperties()
  .getProperty('WEBHOOK_SECRET');

function doPost(e) {
  const signature = e.parameter['X-Webhook-Signature'];
  const expected = computeHmacSha256(e.postData.contents, WEBHOOK_SECRET);

  if (signature !== expected) {
    return ContentService.createTextOutput(
      JSON.stringify({ error: 'Invalid signature' })
    ).setMimeType(ContentService.MimeType.JSON);
  }
  // ... proceed with order processing
}

function computeHmacSha256(payload, secret) {
  const sig = Utilities.computeHmacSha256Signature(payload, secret);
  return sig.map(b => ('0' + (b & 0xff).toString(16)).slice(-2)).join('');
}

Two non-obvious notes: store the secret in PropertiesService.getScriptProperties() (never hardcode), and use constant-time comparison when possible to avoid timing attacks. For most small-business webhooks the timing risk is negligible; for payment webhooks it matters. Stripe and Twilio both publish their exact signature schemes — copy them rather than inventing your own.

Handling Duplicate Webhooks (Idempotency)

Webhook senders retry. If your handler takes 5 seconds and the sender's timeout is 3, the sender assumes failure and resends the same event. Without protection, you log the same Stripe payment two or three times.

The fix is idempotency: track processed webhook IDs in CacheService (or a hidden Sheet for longer retention) and short-circuit duplicates.

function doPost(e) {
  const payload = JSON.parse(e.postData.contents);
  const eventId = payload.id || payload.event_id;

  const cache = CacheService.getScriptCache();
  if (cache.get(`processed:${eventId}`)) {
    return jsonResponse({ status: 'already_processed' });
  }

  processOrder(payload);
  cache.put(`processed:${eventId}`, '1', 3600); // 1 hour TTL
  return jsonResponse({ status: 'success' });
}

Stripe, Twilio, Shopify, and most modern providers always send a unique event ID. Trust the ID, not the timestamp — clocks drift, IDs do not. For long-term replay protection beyond CacheService's 6-hour ceiling, write the ID to a hidden _processed_events sheet and check both layers.

Provider-Specific Webhook Patterns

Different providers send webhooks differently. The shape of doPost(e) changes accordingly:

  • Stripe — Raw JSON body, Stripe-Signature header, requires verification with their signing secret. Read with JSON.parse(e.postData.contents).
  • Twilioapplication/x-www-form-urlencoded, parameters in e.parameter, not e.postData.contents. The body fields are e.parameter.From, e.parameter.Body, etc.
  • Magento — REST event subscriber, JSON body, custom headers from your webapi.xml. Often needs the order entity expanded with ?expand=items to include line items, the same gotcha we covered in our Magento order sync guide.
  • GitHub — JSON body, X-Hub-Signature-256 header for HMAC, X-GitHub-Event header for event type. Many event types — filter by header before parsing the body.
  • Slack — JSON body but requires a response within 3 seconds. Anything heavier must offload work to a queue (a Sheet append plus a separate trigger drain) and return 200 immediately. The 30-second doPost ceiling never gets close.

A small router at the top of your handler is usually enough:

function doPost(e) {
  const provider = detectProvider(e); // checks headers, content-type
  switch (provider) {
    case 'stripe':  return handleStripe(e);
    case 'twilio':  return handleTwilio(e);
    case 'magento': return handleMagento(e);
    default:        return handleGeneric(e);
  }
}

Debugging Webhooks That Silently Fail

The most common Apps Script webhook problem: the sender reports success, your Sheet stays empty, and the script editor shows nothing in its execution history. Three causes account for 90% of these:

  1. Wrong deployment URL. Apps Script gives you both a dev URL (changes every save) and a stable prod URL (created via New deployment). Webhook senders need the prod URL. Check Deploy > Manage deployments and confirm the URL you gave the sender matches.
  2. "Who has access" set to "Only myself". This is the default and silently rejects every external request with a 401. Must be set to "Anyone" for unauthenticated webhook senders. Re-deploy after changing — the existing URL keeps the old setting.
  3. Cold-start latency. The first request after a long quiet period can take 5–8 seconds. Some senders (Slack, Twilio voice callbacks) time out before the response arrives. Pair with a separate "warmup" trigger that hits the URL every 5 minutes during business hours, and the cold-start vanishes.

When debugging, always add console.log(JSON.stringify(e)) as the first line of doPost. The Apps Script Executions panel shows the full event object — headers, parameters, raw body — and reveals which field the sender actually used. The right side of Executions also shows execution time; if a webhook handler runs longer than 25 seconds you are approaching the 30-second hard timeout, and the workaround is the queue pattern documented in our long-running Apps Script jobs guide. For local testing without sending live HTTP, extract the handler body into a pure handleWebhook(payload) function so you can unit-test it — the same separation we recommend in our Apps Script unit testing guide.

Why This Will Change Your Architecture

Once you master doPost(), you unlock real-time digital architecture that costs exactly $0 in server fees.

  • Form submissions from Webflow or WordPress? Push them directly to Sheets.
  • Stripe payment succeeded? Log it via a webhook.
  • Custom internal mobile app? Make it completely Serverless connected straight to Google Workspace.

By bypassing rigid, slow middleware options and coding direct webhooks in Apps Script, you own your logic stream completely.

If you have a massive e-commerce operation running on Magento, and need enterprise-grade, bi-directional syncing between Magento and Google Workspace without the monthly overhead of SaaS integrators, check out our consulting solutions!

Frequently Asked Questions

Do I need a separate Google Cloud project for Apps Script webhooks, or does the Sheet-attached script work?

The script attached to a Sheet works fine for most webhook integrations and is what 95% of small-business setups use. You only need a separate Cloud project when you want to call Apps Script from another script (advanced Execution API), use OAuth for outbound auth, or hit Apps Script execution metrics in Cloud Logging. For ingesting Stripe, Magento, Twilio, or HubSpot webhooks the Sheet-attached script is sufficient — you authenticate the webhook sender via your own HMAC signature, not Google's auth layer.

What is the maximum payload size Apps Script doPost can receive?

50 MB request body, which is the same as UrlFetchApp's outbound limit. In practice, no real webhook ever approaches this — Stripe events average 2–5 KB, Magento orders 8–15 KB, Slack message payloads stay under 50 KB. The size limit is more relevant for file-upload webhooks (image uploads via Twilio MMS, attached invoices). For those, store the asset in Drive via DriveApp and write the URL to your Sheet rather than the file itself.

Can my Apps Script webhook respond fast enough for time-sensitive providers like Slack (3-second deadline)?

Yes, but only if you keep the handler thin. The pattern is: parse the payload, append a queue row to a Sheet, return 200 — total work under one second. A separate time-driven trigger drains the queue every minute. This decouples the synchronous response from the actual processing, and it is the single most reliable webhook pattern at scale.

How do I version my webhook URL when I make breaking changes?

Apps Script lets you publish multiple deployments simultaneously, each with its own stable URL — version 1 stays live for legacy senders while you migrate version 2 in parallel. Use Deploy > New deployment and label them clearly. Both URLs hit the same script project, but you can fork the handler with a version parameter (?v=2 in the URL or a custom header) to route requests. Once all senders are migrated, archive the old deployment to revoke its URL.

Does my Apps Script webhook count toward the daily UrlFetchApp quota?

No. Inbound webhooks (data coming into Apps Script via doPost) are completely separate from outbound UrlFetchApp calls. The 20,000-call free / 100,000 Workspace daily limit applies only to UrlFetchApp.fetch — your script reaching out to other APIs. Your webhook can receive unlimited inbound traffic, though heavy sustained inbound for hours can affect overall execution time, which is its own daily quota (90 minutes free / 6 hours Workspace).

Stay Updated

Get the latest insights on AI, e-commerce, and Magento delivered to your inbox.