fx APIs & Web Apps

Build a web app with doGet/doPost

⏱ 25 min

What you'll learn

  • Turning a script into a web page or a JSON API
  • doGet(e) and doPost(e) and their parameters
  • Deploying, access settings, and updating versions
  • A small "check order status" app backed by a sheet

Concept

Two special functions

When a script is deployed as a web app, Google gives it a URL. Visiting the URL runs:

  • doGet(e) — for normal visits and links (GET requests)
  • doPost(e) — when a form or another program sends data (POST requests)

Whatever they return is what the visitor sees.

Returning a web page

Add an HTML file named Index:

<!DOCTYPE html>
<html>
  <head><base target="_top"><meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body style="font-family: Arial; max-width: 420px; margin: 24px auto;">
    <h2>Check your order</h2>
    <input id="orderNo" placeholder="Order number, e.g. ORD-1042">
    <button onclick="check()">Check</button>
    <p id="result"></p>
    <script>
      function check() {
        const no = document.getElementById('orderNo').value.trim();
        document.getElementById('result').textContent = 'Checking…';
        google.script.run
          .withSuccessHandler(msg => { document.getElementById('result').textContent = msg; })
          .getStatus(no);
      }
    </script>
  </body>
</html>

And in Code.gs:

const SHEET_ID = 'PASTE_SHEET_ID';

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('Order status')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function getStatus(orderNo) {
  if (!orderNo) return 'Please enter an order number.';
  const rows = SpreadsheetApp.openById(SHEET_ID).getSheetByName('Orders').getDataRange().getValues();
  const match = rows.find(r => String(r[0]).toUpperCase() === orderNo.toUpperCase());
  return match ? `Status: ${match[3]}` : 'Order not found.';
}

Only the Status is returned — never send the whole row (phone numbers, amounts) to a public page.

Returning JSON (a mini API)

function doGet(e) {
  const no = (e.parameter.order || '').toUpperCase();   // ?order=ORD-1042
  const rows = SpreadsheetApp.openById(SHEET_ID).getSheetByName('Orders').getDataRange().getValues();
  const match = rows.find(r => String(r[0]).toUpperCase() === no);

  const body = match ? { order: match[0], status: match[3] } : { error: 'not found' };
  return ContentService.createTextOutput(JSON.stringify(body))
    .setMimeType(ContentService.MimeType.JSON);
}

Visit https://script.google.com/macros/s/<DEPLOYMENT_ID>/exec?order=ORD-1042.

e.parameter holds query-string values. A project can have only one doGet, so choose either the HTML version or the JSON version — or check a parameter (e.g. e.parameter.format === 'json') to return one or the other.

Receiving data: doPost

function doPost(e) {
  try {
    const data = JSON.parse(e?.postData?.contents || '{}');
    const token = PropertiesService.getScriptProperties().getProperty('WEBHOOK_TOKEN');
    if (!token || data.token !== token) throw new Error('Unauthorized');
    for (const key of ['name', 'phone', 'source']) {
      if (typeof data[key] !== 'string' || !data[key].trim() || data[key].length > 200) {
        throw new Error('Invalid ' + key);
      }
    }
    // Keep submitted strings as text, never spreadsheet formulas.
    const text = value => /^[=+@-]/.test(value.trim()) ? "'" + value.trim() : value.trim();
    SpreadsheetApp.openById(SHEET_ID).getSheetByName('Leads')
      .appendRow([new Date(), text(data.name), text(data.phone), text(data.source)]);
    return ContentService.createTextOutput(JSON.stringify({ ok: true }))
      .setMimeType(ContentService.MimeType.JSON);
  } catch (err) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: String(err.message) }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

Set WEBHOOK_TOKEN in Script Properties first. A trusted server-side caller sends that token in the JSON body. Never embed it in public browser code. The response uses ok: false for errors; callers must inspect that field. Test with synthetic orders and restrict access while learning.

Deploying

  1. Deploy → New deployment → Select type: Web app.
  2. Execute as: Me (the script uses your access to the sheet).
  3. Who has access: Only myself, Anyone with a Google account, or Anyone.
  4. Click Deploy and copy the /exec URL.

Updating: saving code does NOT change the live app. Go to Deploy → Manage deployments → Edit (pencil) → Version: New version → Deploy. The URL stays the same. For testing, Test deployments gives a /dev URL that always runs your latest saved code (only for project editors).

Start with Only myself and synthetic data. A real customer-facing order lookup also needs authentication and authorization; an order number alone is not proof of ownership.

Security checklist

  • Anyone access means anyone with the URL can call it — validate every input.
  • Return only the fields the visitor needs.
  • For doPost from your own systems, require a shared secret, e.g. if (data.token !== PropertiesService.getScriptProperties().getProperty('WEBHOOK_TOKEN')) return ....
  • Use LockService if many requests may append at the same moment.

Key points

  • doGet / doPost + Deploy = a URL that runs your code.
  • HtmlService for pages, ContentService for JSON.
  • Deploy a new version after every change.
  • Treat public web apps like any public website: validate inputs, expose minimal data.

Exercises

mediumPractice 1

Add a format parameter so that ?order=ORD-1042&format=json returns JSON and no parameter returns the HTML page — using a single doGet.

Quiz

You saved new code but the live URL still shows the old version. Why? a) Browser cache only b) You didn't deploy a new version c) doGet is broken
You didn't deploy a new version
Where are query-string values like ?order=ORD-1 found? a) e.postData b) e.parameter c) e.range
e.parameter
Build a web app with doGet/doPost · Google Apps Script | ExcelWalaa