Build a Contact Form Handler with Apps Script Web App

You don't need a server to handle contact form submissions. A deployed Apps Script Web App can receive POST requests, save the data to a Sheet, and email you a notification — all for free.

How it works

  1. Your HTML form POSTs data to a deployed Apps Script Web App URL.
  2. The doPost(e) function receives the form data.
  3. The script saves it to a Google Sheet and emails you.

Step 1 — Create the Apps Script Web App

function doPost(e) { try { const params = e.parameter; const name = params.name || ''; const email = params.email || ''; const message = params.message || ''; // Save to Sheet const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); if (sheet.getLastRow() === 0) { sheet.appendRow(['Timestamp', 'Name', 'Email', 'Message']); } sheet.appendRow([new Date(), name, email, message]); // Email notification GmailApp.sendEmail( Session.getActiveUser().getEmail(), `New Contact Form Submission from ${name}`, `Name: ${name}\nEmail: ${email}\nMessage:\n${message}` ); return ContentService .createTextOutput(JSON.stringify({ success: true })) .setMimeType(ContentService.MimeType.JSON); } catch (err) { return ContentService .createTextOutput(JSON.stringify({ success: false, error: err.message })) .setMimeType(ContentService.MimeType.JSON); } }

Step 2 — Deploy as a Web App

  1. Click Deploy > New deployment.
  2. Select Web App.
  3. Set Execute as: Me.
  4. Set Who has access: Anyone.
  5. Click Deploy and copy the Web App URL.

Step 3 — Create your HTML contact form

<form id="contactForm"> <input type="text" name="name" placeholder="Your Name" required /> <input type="email" name="email" placeholder="Your Email" required /> <textarea name="message" placeholder="Your Message" required></textarea> <button type="submit">Send</button> <p id="status"></p> </form> <script> const SCRIPT_URL = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec'; document.getElementById('contactForm').addEventListener('submit', async function(e) { e.preventDefault(); const status = document.getElementById('status'); status.textContent = 'Sending...'; const formData = new FormData(this); const params = new URLSearchParams(formData).toString(); try { const res = await fetch(SCRIPT_URL, { method: 'POST', body: params, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); const data = await res.json(); status.textContent = data.success ? 'Message sent! We\'ll be in touch.' : 'Error: ' + data.error; } catch (err) { status.textContent = 'Something went wrong. Please try again.'; } }); </script>

Handle JSON payloads instead of form data

If your frontend sends JSON (common with fetch/axios):

function doPost(e) { let data; try { data = JSON.parse(e.postData.contents); } catch (err) { data = e.parameter; // Fall back to form-encoded } const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); sheet.appendRow([new Date(), data.name, data.email, data.message]); return ContentService .createTextOutput(JSON.stringify({ success: true })) .setMimeType(ContentService.MimeType.JSON); }

Add basic spam protection

function doPost(e) { const params = e.parameter; // Honeypot field — bots fill it, humans don't if (params.website) { return ContentService .createTextOutput(JSON.stringify({ success: true })) // Fake success to confuse bots .setMimeType(ContentService.MimeType.JSON); } // Rate limiting: max 10 submissions per hour per IP isn't possible in Apps Script, // but you can check for duplicate submissions within a short window const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const recentData = sheet.getLastRow() > 1 ? sheet.getRange(sheet.getLastRow(), 1, 1, 3).getValues()[0] : null; if (recentData) { const lastTime = new Date(recentData[0]); const lastEmail = recentData[2]; const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); if (lastEmail === params.email && lastTime > fiveMinutesAgo) { return ContentService .createTextOutput(JSON.stringify({ success: false, error: 'Duplicate submission' })) .setMimeType(ContentService.MimeType.JSON); } } sheet.appendRow([new Date(), params.name, params.email, params.message]); return ContentService .createTextOutput(JSON.stringify({ success: true })) .setMimeType(ContentService.MimeType.JSON); }

Tips

  • Every time you edit the script, you must create a new deployment (not update the existing one) for changes to take effect.
  • Web Apps deployed as "Anyone" don't require users to log in — perfect for public contact forms.
  • Apps Script Web Apps have a cold start delay of a few seconds on the first request. Subsequent requests are faster.
  • For CORS issues, Apps Script automatically adds the necessary headers when returning ContentService output.