How to submit an HTML form to Google Sheets
A contact form, a waitlist, an event sign-up, a survey: the simplest place to collect the answers is a spreadsheet everyone on the team can open. The usual ways to get there are a backend that talks to the Google Sheets API (a Google Cloud project, OAuth or a service account) or a Google Apps Script web app you write, deploy and redeploy. Both are more work than the form itself.
With SheetDB the form posts straight to an API URL and every submission becomes a new row in your Google Sheet. No backend, no Apps Script, no OAuth and no Google credentials in your page, so it works on any static site: GitHub Pages, Netlify, Vercel or a single HTML file. This guide starts with a form that works without any JavaScript, then adds a thank-you message, timestamps, a JSON variant for front-end frameworks and spam protection.
Quick start: this is the whole thing. Swap the URL in action for your API URL and each submission is saved as a row.
<form method="post" action="https://sheetdb.io/api/v1/58f61be4dda40">
<input type="hidden" name="data[id]" value="INCREMENT">
Name: <input name="data[name]" required>
Age: <input name="data[age]" type="number">
Comment: <input name="data[comment]">
<button type="submit">Submit</button>
</form>
The examples write to this demo spreadsheet through the API https://sheetdb.io/api/v1/58f61be4dda40. Its first tab has the columns id, name, age and comment. The demo resets every 15 minutes, so go ahead and submit the live forms below, then open the spreadsheet to see your row.
Step 1: prepare the sheet and create an API
- Create a Google Sheet and type the names of the fields you want to collect in the first row, for example
name,email,message. Each column becomes one form field. - Sign in to SheetDB with the Google account that owns the sheet, click Create new API and paste the spreadsheet URL.
- Copy the API URL,
https://sheetdb.io/api/v1/<your id>. That is theactionof your form. The spreadsheet stays private; SheetDB writes to it with your Google account.
Step 2: name the form fields after the columns
The only rule: each field's name is the column name wrapped in data[...], for example name="data[email]" for a column called email. Names are case-sensitive, and fields that match no column are ignored. Here is the quick-start form running live; submit it and you will see the raw API response:
<form method="post" action="https://sheetdb.io/api/v1/58f61be4dda40">
<input type="hidden" name="data[id]" value="INCREMENT">
Name: <input name="data[name]" required>
Age: <input name="data[age]" type="number">
Comment: <input name="data[comment]">
<button type="submit">Submit</button>
</form>
This works with no JavaScript at all. The hidden data[id] field with the value INCREMENT tells SheetDB to put the next number in the id column. After submitting, the browser shows the API response, {"created":1}: proof that one row was added, but not a page you want visitors to land on. Step 3 fixes that.
Any form control works as long as it sends one value per name: text, e-mail, number and date inputs, textareas, selects, radio buttons and a single checkbox. An unchecked checkbox sends nothing, so its cell stays empty.
<input type="email" name="data[email]" required>
<textarea name="data[message]"></textarea>
<select name="data[plan]">
<option>Free</option>
<option>Pro</option>
</select>
<label><input type="radio" name="data[contact]" value="phone"> Phone</label>
<label><input type="radio" name="data[contact]" value="email"> E-mail</label>
<label><input type="checkbox" name="data[newsletter]" value="yes"> Send me the newsletter</label>
Step 3: show a thank-you message instead of JSON
A few lines of JavaScript send the same form in the background with fetch(), keep the visitor on the page and show a message. The script checks response.ok, because fetch() only fails on network errors, and shows the error otherwise. Try it:
<form id="sheetdb-form" method="post" action="https://sheetdb.io/api/v1/58f61be4dda40">
<input type="hidden" name="data[id]" value="INCREMENT">
Name: <input name="data[name]" required>
Age: <input name="data[age]" type="number">
Comment: <input name="data[comment]">
<button type="submit">Submit</button>
<p id="sheetdb-form-status" role="status"></p>
</form>
<script>
const form = document.getElementById('sheetdb-form');
const status = document.getElementById('sheetdb-form-status');
form.addEventListener('submit', (event) => {
event.preventDefault();
status.textContent = 'Sending...';
fetch(form.action, { method: 'POST', body: new FormData(form) })
.then((response) => {
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
return response.json();
})
.then(() => {
form.reset();
status.textContent = 'Thank you! Your answer was saved.';
})
.catch((error) => {
status.textContent = 'Something went wrong: ' + error.message;
});
});
</script>
To send people to a thank-you page instead, replace the line that sets the success message with a redirect:
window.location.href = 'https://yourwebsite.com/thank-you';
The same spot is where you fire an analytics event or a conversion pixel, since you know the row was saved.
Add a timestamp and an auto-increment id
Hidden fields add data the visitor does not type. SheetDB recognises a few special values: INCREMENT becomes the highest number in the column plus one, DATETIME the current date and time, and TIMESTAMP a Unix timestamp. Add the matching columns (id, created_at, source) to the first row of the sheet first:
<!-- the next number in the "id" column -->
<input type="hidden" name="data[id]" value="INCREMENT">
<!-- the date and time of the submission, e.g. 2026-10-10 14:05:31 -->
<input type="hidden" name="data[created_at]" value="DATETIME">
<!-- where the visitor came from -->
<input type="hidden" name="data[source]" value="landing-page-a">
A fixed hidden value such as source is handy when the same sheet collects entries from several pages or campaigns. The full list of options is in the create docs.
Send JSON from React, Vue or any JavaScript
If your form is a component rather than a <form> with an action, post JSON instead. The body is a data array with one object per row, keyed by column name:
// e.g. in a React, Vue or Svelte submit handler
async function saveToSheet({ name, age, comment }) {
const response = await fetch('https://sheetdb.io/api/v1/58f61be4dda40', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [{ id: 'INCREMENT', name, age, comment }],
}),
});
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
return response.json(); // { created: 1 }
}
The API sends CORS headers, so this works from any domain. The same request can come from your server, a serverless function or a Node.js or PHP backend when you want to validate the data first.
Protect the form and the sheet
The API URL sits in your page's HTML, so anyone can find it. Make sure it can only do what the form needs:
- Permissions. In the SheetDB API settings keep Create (POST) on and switch off Read (GET), Search (GET), Update (PATCH/PUT) and Delete (DELETE). Visitors can then add rows but cannot read the answers of others or change anything. See the permissions docs.
- A separate API for the form. If the same spreadsheet also powers a page or an app, create a second API for it with write-only permissions, and keep the read API separate.
- A honeypot against bots. Add a field that people never see and drop submissions that fill it in. It costs nothing and stops most simple spam bots.
<form id="sheetdb-form" method="post" action="https://sheetdb.io/api/v1/58f61be4dda40">
<!-- ...your data[...] fields... -->
<!-- Bots fill in every field. People never see this one. -->
<input type="text" name="website" tabindex="-1" autocomplete="off"
style="position: absolute; left: -9999px" aria-hidden="true">
<button type="submit">Submit</button>
</form>
<script>
const form = document.getElementById('sheetdb-form');
form.addEventListener('submit', (event) => {
event.preventDefault();
if (form.elements.website.value !== '') {
return; // a bot filled in the hidden field: drop the submission
}
fetch(form.action, { method: 'POST', body: new FormData(form) })
.then((response) => {
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
form.reset();
})
.catch((error) => console.error(error));
});
</script>
The honeypot field has no data[...] name, so it never reaches the sheet. For stronger protection, such as a CAPTCHA you verify on a server, send the form to a small serverless function that checks it and then posts to the API with a Bearer token (see the authentication docs).
Do something with the row after it is saved
A form that writes to a sheet is often the first step of a longer process: send a confirmation e-mail, notify a Slack channel, create an invoice. The same API works from automation tools, and the guide on automating Google Sheets with n8n or Make shows how to read the new rows and write results back. If the form lives inside an app built in Bubble or Adalo, those guides configure the same POST request in the tool's own connector instead of HTML. To show the collected entries on a page, for example a public list of sign-ups, see how to display Google Sheets data on a website.
Limitations
- File uploads are not stored. The API writes text to cells. Upload files to a storage service and save the link in the sheet.
- Checkbox groups need care. Each field name maps to one cell. Give each checkbox its own column, or join the checked values into one string in JavaScript before sending.
- Validation happens in the browser. HTML attributes such as
requiredandtype="email"help honest visitors, but anyone can post to the API directly. If the data must be trusted, validate it on a server first. - Request quota. Each submission is one API request and counts towards the monthly quota of your plan.
FAQ
Can I send an HTML form to Google Sheets without a backend?
Yes. The form posts straight to your SheetDB API URL, and SheetDB writes the row to the spreadsheet with the Google account you connected. There is no server, no serverless function and no Google credential in your page, so it works on any static host: GitHub Pages, Netlify, Vercel, an S3 bucket or a plain HTML file.
How is this different from using Google Apps Script?
With Apps Script you write a doPost() function in the spreadsheet, deploy it as a web app that anyone can run, and redeploy it after every change. You also handle the script's redirecting responses and CORS behaviour in your JavaScript and stay within Apps Script quotas. With SheetDB there is no script to write or deploy: the form posts to a URL that returns plain JSON with proper CORS headers, and the same API can also read, update and delete rows later. See the full SheetDB vs Google Apps Script comparison.
Can I add a timestamp to every submission?
Yes. Add a column such as created_at to the sheet and a hidden input to the form named data[created_at] with the value DATETIME. SheetDB replaces DATETIME with the current date and time (TIMESTAMP gives a Unix timestamp). INCREMENT in an id column works the same way and assigns the next number.
What happens if a field name does not match a column?
It is silently skipped. Column names are case-sensitive, so data[Email] does not fill a column called email. If a row arrives with empty cells, compare the name attributes of your inputs with the first row of the sheet.
How do I protect the form from spam?
The API URL is visible in your page source, so treat it as public. In the SheetDB API settings keep only Create (POST) enabled for the API the form uses, so nobody can read, change or delete rows through it. A honeypot field (a hidden input that people leave empty and bots fill in, checked in your submit handler) stops simple bots. If you need real verification such as a CAPTCHA, put a small serverless function between the form and the API and keep the API protected with a Bearer token.
Is it free to save form submissions to Google Sheets?
Yes, for small forms. The SheetDB free plan includes 2 APIs and 500 requests a month with no credit card; each submission is one request. Paid plans start when a form collects more entries than that.
Prefer to watch? This short video builds the form step by step:
Save your form to Google Sheets
Sign in with Google, paste the spreadsheet URL and use the API URL as your form's action. Free plan, no credit card, no backend.
Create free APIHave a question?
Ask about the API or your sheet. Developers answer, usually the same day.