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
- Deploy → New deployment → Select type: Web app.
- Execute as: Me (the script uses your access to the sheet).
- Who has access: Only myself, Anyone with a Google account, or Anyone.
- 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
doPostfrom your own systems, require a shared secret, e.g.if (data.token !== PropertiesService.getScriptProperties().getProperty('WEBHOOK_TOKEN')) return .... - Use
LockServiceif many requests may append at the same moment.
Key points
doGet/doPost+ Deploy = a URL that runs your code.HtmlServicefor pages,ContentServicefor JSON.- Deploy a new version after every change.
- Treat public web apps like any public website: validate inputs, expose minimal data.