All articles

How to Send HTML Form Data to Google Sheets for Free

a cover of a blog post that talks about sending contact form data to google sheets

If you have a contact form, an opt-in widget, or a custom HTML form on your site, you can route every submission straight into a Google Sheet without a database, a Zapier subscription, or a paid backend. Three free methods get you there. Two need code. One does not.

This guide gives you the working Apps Script (paste-ready), the no-code path, and a side-by-side comparison so you know which one to pick before you start.

Quick answer

You can push HTML form submissions into a Google Sheet for free with three methods. Pick one in 30 seconds:

  • Google Forms (2 minutes, no code): use it when the form does not need to live on your own domain.
  • Google Apps Script (20-40 minutes, paste-ready code below): use it when you already have an HTML form on your site and are fine maintaining a script.
  • No-code builder like Formester (5 minutes, no code): use it when you want spam control, conditional logic, and Sheets sync in one place.
  • Apps Script caps at roughly one Sheets write per second per user, which is plenty for a contact form.
  • Never store payment data or passwords in Sheets. It is a great inbox, not a vault.

Which method should you pick?

Three free routes from form submission to a Google Sheet row. Match the method to where you already are.

Method 1

Google Forms

Best for: A form you do not need to embed on your own site.


  • 2-minute setup, no code at all.
  • Free with any Google account.
  • Responses auto-link to a Sheet.
  • Lives on a Google URL, not your domain.
  • Limited design control.
Method 2

Google Apps Script

Best for: A custom HTML form you already have on your site.


  • Free forever, no usage cap on Sheets writes (within Google quotas).
  • Paste-ready code, no edits required.
  • Stays on your domain, no third-party branding.
  • 20-40 minute setup.
  • You maintain the script and the deployment.

Decision rule

  • If the form already exists in HTML and you are fine maintaining a script, use Apps Script.
  • If you do not need to embed the form on your own domain, use Google Forms.
  • If you want zero maintenance, spam control, conditional logic, and Google Sheets sync in one place, use a no-code form builder like Formester.

Method 2: Google Apps Script, full working setup

Method 1 is one click in Google Forms. Method 3 is a no-code toggle in Formester. The only method that needs a real walkthrough is Method 2. Here it is, in full.

Paste-ready code that handles any field name in your HTML form, sanitizes against CSV formula injection, and works with both application/json and standard form-encoded submissions.

  1. Create the Sheet

    Open Google Sheets and create a new spreadsheet. Name the tab Submissions. In row 1, add column headers that match the name attributes on your form inputs. Add timestamp as the first column. The script reads row 1 as the source of truth for column mapping, so a typo in a header is a typo in the data.

    Example row 1: timestamp | name | email | message

  2. Open Apps Script and paste the code

    Inside the Sheet, go to Extensions, then Apps Script. Delete the boilerplate and paste this:

    Code.gsconst SHEET_NAME = 'Submissions';
    const SCRIPT_PROP = PropertiesService.getScriptProperties();
    
    

    function initialSetup() { const activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet(); SCRIPT_PROP.setProperty('key', activeSpreadsheet.getId()); }

    function doPost(e) { const lock = LockService.getScriptLock(); lock.tryLock(10000);

    try { const doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty('key')); const sheet = doc.getSheetByName(SHEET_NAME);

    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const nextRow = sheet.getLastRow() + 1;
    
    // Honeypot: if a hidden field named "website" is filled, drop the submission.
    if (e.parameter.website) {
      return ContentService.createTextOutput(JSON.stringify({ result: 'ignored' }))
        .setMimeType(ContentService.MimeType.JSON);
    }
    
    const sanitize = (value) => {
      if (typeof value !== 'string') return value;
      // Block CSV / formula injection
      return /^[=+\-@]/.test(value) ? "'" + value : value;
    };
    
    const newRow = headers.map((header) => {
      if (header === 'timestamp') return new Date();
      return sanitize(e.parameter[header] || '');
    });
    
    sheet.getRange(nextRow, 1, 1, newRow.length).setValues([newRow]);
    
    return ContentService
      .createTextOutput(JSON.stringify({ result: 'success', row: nextRow }))
      .setMimeType(ContentService.MimeType.JSON);
    

    } catch (err) { return ContentService .createTextOutput(JSON.stringify({ result: 'error', error: err.toString() })) .setMimeType(ContentService.MimeType.JSON); } finally { lock.releaseLock(); } }

    Save the script. Then in the function dropdown at the top of the editor, pick initialSetup and click Run once. Approve the OAuth prompt when Google asks. You will only do this once.

  3. Deploy as a web app

    Click Deploy, then New deployment. Pick Web app. Set Execute as: Me. Set Who has access: Anyone. Click Deploy. Copy the long URL that ends in /exec. That URL is your form endpoint.

    Heads-up: every time you change the script, redeploy and grab the new URL. Old URLs keep working at the old script version, which is the most common cause of "I updated my code and nothing changed" complaints.
  4. Wire up your HTML form

    Drop this into your page. Replace PASTE_YOUR_DEPLOYMENT_URL_HERE with the URL from step 3.

    index.html<form id="contact-form">
      <input type="text" name="name" required placeholder="Name" />
      <input type="email" name="email" required placeholder="Email" />
      <textarea name="message" required placeholder="Message"></textarea>
      <!-- Honeypot: hidden from humans, filled by bots -->
      <input type="text" name="website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" />
      <button type="submit">Send</button>
    </form>
    
    

    <script> const form = document.getElementById('contact-form'); const endpoint = 'PASTE_YOUR_DEPLOYMENT_URL_HERE';

    form.addEventListener('submit', async (e) => { e.preventDefault(); const data = new FormData(form); try { await fetch(endpoint, { method: 'POST', body: data }); form.reset(); alert('Thanks. We will be in touch.'); } catch (err) { alert('Something went wrong. Try again in a minute.'); } }); </script>

    Submit a test entry. The row should appear in your Sheet within a second.

    Rate limit to know about: Google caps the Sheets-backed Apps Script at roughly 100 requests per 100 seconds per user, or one write per second. For a contact form that is plenty. If you expect bursts (a launch, a paid ad campaign), queue submissions or use a builder that handles retries.
No-code path

Send HTML form data to Google Sheets, without writing a single line of code

Drop in a Formester form, connect Google Sheets, done. Two-way sync, free plan, no script to maintain.

Start free on Formester

Free forever planNo credit cardSetup in 2 minutes

Share this article
FAQ

Frequently asked questions

Common questions about routing HTML form data into Google Sheets for free.

Ready to build your perfect form?

Formester is the easiest way to create forms, collect data and automate your workflow