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
Your HTML form POSTs data to a deployed Apps Script Web App URL.
The doPost(e) function receives the form data.
The script saves it to a Google Sheet and emails you.
Step 1 — Create the Apps Script Web App
functiondoPost(e){try{const params = e.parameter;const name = params.name||'';const email = params.email||'';const message = params.message||'';// Save to Sheetconst sheet =SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();if(sheet.getLastRow()===0){ sheet.appendRow(['Timestamp','Name','Email','Message']);} sheet.appendRow([newDate(), name, email, message]);// Email notificationGmailApp.sendEmail(Session.getActiveUser().getEmail(),`New Contact Form Submission from ${name}`,`Name: ${name}\nEmail: ${email}\nMessage:\n${message}`);returnContentService.createTextOutput(JSON.stringify({success:true})).setMimeType(ContentService.MimeType.JSON);}catch(err){returnContentService.createTextOutput(JSON.stringify({success:false,error: err.message})).setMimeType(ContentService.MimeType.JSON);}}
Step 2 — Deploy as a Web App
Click Deploy > New deployment.
Select Web App.
Set Execute as: Me.
Set Who has access: Anyone.
Click Deploy and copy the Web App URL.
Step 3 — Create your HTML contact form
<formid="contactForm"><inputtype="text"name="name"placeholder="Your Name"required/><inputtype="email"name="email"placeholder="Your Email"required/><textareaname="message"placeholder="Your Message"required></textarea><buttontype="submit">Send</button><pid="status"></p></form><script>constSCRIPT_URL='https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec';document.getElementById('contactForm').addEventListener('submit',asyncfunction(e){ e.preventDefault();const status =document.getElementById('status'); status.textContent='Sending...';const formData =newFormData(this);const params =newURLSearchParams(formData).toString();try{const res =awaitfetch(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):
functiondoPost(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([newDate(), data.name, data.email, data.message]);returnContentService.createTextOutput(JSON.stringify({success:true})).setMimeType(ContentService.MimeType.JSON);}
Add basic spam protection
functiondoPost(e){const params = e.parameter;// Honeypot field — bots fill it, humans don'tif(params.website){returnContentService.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 windowconst sheet =SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();const recentData = sheet.getLastRow()>1? sheet.getRange(sheet.getLastRow(),1,1,3).getValues()[0]:null;if(recentData){const lastTime =newDate(recentData[0]);const lastEmail = recentData[2];const fiveMinutesAgo =newDate(Date.now()-5*60*1000);if(lastEmail === params.email&& lastTime > fiveMinutesAgo){returnContentService.createTextOutput(JSON.stringify({success:false,error:'Duplicate submission'})).setMimeType(ContentService.MimeType.JSON);}} sheet.appendRow([newDate(), params.name, params.email, params.message]);returnContentService.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.