Send an HTML form to Google Sheets without a backend
A static page, a form tag, and a permanent endpoint. Rows land in your sheet and you never deploy a server.
The problem: HTML forms need somewhere to POST
An HTML form is complete except for one thing: the action attribute has to point at a server that accepts the POST. On a static site - GitHub Pages, Netlify, S3, a plain folder behind a CDN - there is no server. The classic fixes are writing a serverless function, wiring up Apps Script, or bolting on a paid form service that emails you the results and calls it a day.
SheetLink Forms replaces all of that with a permanent endpoint. Every form you create gets a URL of the shape https://sheetlinkforms.com/f/slf_yourtoken. Point your form's action at it, and each submission is screened, mapped to columns, and appended as a row in a Google Sheet you own. The sheet is the working system, not a notification inbox. You can see the whole pipeline on how it works.
The five-minute version
Here is a complete, working contact form. Nothing else is required - no JavaScript, no build step, no API key in the page:
<form action="https://sheetlinkforms.com/f/slf_yourtoken" method="POST">
<input name="name" placeholder="Name" required>
<input name="email" type="email" placeholder="Email" required>
<textarea name="message" placeholder="How can we help?"></textarea>
<input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">
<button type="submit">Send</button>
</form>The hidden _slhp input is the honeypot. Humans never see it and never fill it; bots that auto-complete every field do, and those submissions are marked as spam outright. Keep it in every form you ship.
Steps, end to end:
- Create a form in the dashboard and connect Google Sheets with the one-click OAuth flow. The connection uses the
drive.filescope, so SheetLink Forms can only touch sheets you explicitly pick in the Google picker or create inside the product - never the rest of your Drive. - Copy the endpoint URL into your form's
action. - Submit a test. The row appears in your sheet; if the sheet was empty, a header row is seeded first and rows append under it.
What happens after submit
A plain HTML POST gets a 303 redirect. By default that lands on a hosted thank-you page; you can set a custom redirect URL per form so visitors return to your own site.
If you would rather stay on the page, submit with fetch instead. Send Content-Type: application/json (or an Accept: application/json header) and the endpoint answers with JSON instead of redirecting:
const res = await fetch("https://sheetlinkforms.com/f/slf_yourtoken", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Ada", email: "ada@example.com", _slhp: "" })
});
const data = await res.json(); // { "ok": true, "id": "..." }Delivery to the sheet itself is asynchronous: a worker writes directly to the Google Sheets API and retries at 5 minutes, 30 minutes, and 2 hours if Google is briefly unreachable. Every attempt shows up in the delivery log in the dashboard.
Field names map to column headers automatically
You do not configure a mapping to get started. Field names that match your sheet's column headers map automatically, and the match is case and punctuation insensitive: Email, email, and E-mail all land in an "Email" column. Name your inputs the way you want your columns labeled and the shape takes care of itself.
When names and headers genuinely differ - a legacy form with fname posting into a "First name" column - set an explicit per-field mapping in the dashboard. Automatic matching still covers everything you did not map by hand.
Optional: one script tag for AJAX and attribution
Adding the embed upgrades the same form without changing your markup:
<script src="https://sheetlinkforms.com/sl.js" data-token="slf_yourtoken"></script>sl.js binds any form marked data-sheetlink and any form whose action already points at /f/. It converts the submit to AJAX with inline success text, injects the honeypot for you, adds a timing signal that helps separate humans from scripts, and captures utm_* parameters plus ad click IDs (gclid, fbclid, and friends) from the URL, persisting them in localStorage across pages so they arrive with the submission. If you run ads, that last part matters - see capturing click IDs in forms.
Edge cases worth knowing
- Body size: requests are capped at 256 KB. Plenty for text fields; not a file upload channel.
- File inputs: multipart posts are accepted, but file parts are currently dropped. The text fields still deliver.
- Formula injection: values starting with
=,+,-, or@are escaped before they reach a cell, so a malicious submission cannot plant a live formula in your sheet. - Spam handling: suspicious submissions are quarantined for one-click review in the dashboard - approve delivers the row. Nothing is silently dropped; only the honeypot marks spam outright.
Troubleshooting
The endpoint answers with plain HTTP status codes:
404- unknown token. Re-copy the endpoint from the dashboard.403- the origin is not on your allowlist, or a Turnstile challenge failed. Check the form's origin settings.410- the form is paused.413- body over 256 KB.429- rate limited (per-form or per-IP). Slow the sender down.
If the POST returns 200 but no row appears, check the delivery log first, then the quarantine queue. Want to try it before wiring your own? The live demo posts a real form into a public sheet, and the docs cover every code path. SheetLink Forms is free during beta - grab an invite from the waitlist.
FAQ
Do I need any JavaScript at all?
No. A plain <form method="POST"> pointed at your endpoint works with zero scripts - submissions 303-redirect to a thank-you page. The sl.js embed is optional and adds AJAX submits, attribution capture, and automatic honeypot injection.
Can SheetLink Forms read the rest of my Google Drive?
No. The Google connection uses the drive.file scope, which grants access only to spreadsheets you pick in the Google picker or create inside the product. Everything else in your Drive is invisible to it.
What happens if Google Sheets is briefly unavailable?
Delivery is asynchronous and retries automatically at 5 minutes, 30 minutes, and 2 hours. Each attempt is recorded in the delivery log, so a transient outage costs you latency, not data.
Does this work on Netlify, GitHub Pages, or any static host?
Yes. The form posts cross-origin to sheetlinkforms.com, so the page can live anywhere - static hosts, CDNs, site builders, or a local file during development. If you lock the form to specific origins with the allowlist, add your production domain there.
How much does it cost?
SheetLink Forms is free during beta, with access by invite - join the waitlist on the homepage. There are no per-submission fees, ever. Planned post-beta pricing (not final): a free tier around 50 submissions a month and an unmetered paid plan around $15-19/month.
Related guides: Forms to a spreadsheet without Zapier (and why you might want that) · Capture gclid (and every other click ID) in your forms · Send form submissions to Excel Online automatically