Why "form to Sheets" without Zapier
Zapier's Sheets integration is fast to set up but the math gets ugly. Their free tier is 100 tasks/month, then $19.99/mo for 750 tasks (Starter), then $49/mo for 2,000 tasks (Pro). Each form submission is one task. A real product crosses 750 submissions/month within weeks of launch.
The methods below cost $0 to a few dollars a month, run with no third-party SaaS in the middle, and don't throttle when traffic spikes.
Method 1: Apps Script Web App (free)
Apps Script is Google's in-browser JavaScript runtime that has direct access to Sheets, Gmail, Drive, and Calendar without OAuth. Publishing a script as a Web App gives you a public URL that accepts POSTs and writes to your sheet.
Step 1. Create a Google Sheet with header row:
A1: timestamp
B1: name
C1: email
D1: message
E1: sourceStep 2. Open Extensions → Apps Script and replace the default code with:
function doPost(e) {
// Apps Script Web App endpoint for HTML form submissions
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Sheet1');
// Apps Script gives you e.parameter for form-encoded POSTs
// and e.postData.contents for raw JSON
let data;
if (e.postData && e.postData.type === 'application/json') {
data = JSON.parse(e.postData.contents);
} else {
data = e.parameter;
}
// honeypot check
if (data.botcheck) {
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}
sheet.appendRow([
new Date(),
data.name || '',
data.email || '',
data.message || '',
data.source || 'website',
]);
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}
// Optional: handle CORS preflight from browsers
function doGet() {
return ContentService
.createTextOutput('OK')
.setMimeType(ContentService.MimeType.TEXT);
}Step 3. Click Deploy → New deployment. Choose Web app. Set:
- Execute as: Me (your account)
- Who has access: Anyone
Copy the deployment URL (looks like https://script.google.com/macros/s/.../exec).
Step 4.Point your form at it. Note: Apps Script doesn't set CORS headers, so browser POSTs from a different origin need mode: 'no-cors' with FormData:
<form id="contact">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<input name="botcheck" type="checkbox" style="display:none" tabindex="-1" />
<button type="submit">Send</button>
</form>
<script>
const SCRIPT_URL = 'https://script.google.com/macros/s/AKfycb.../exec';
document.getElementById('contact').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
await fetch(SCRIPT_URL, {
method: 'POST',
mode: 'no-cors',
body: data,
});
e.target.innerHTML = '<p>Thanks — added to the sheet.</p>';
});
</script>The no-corsmode means you can't read the response — Apps Script runs the write either way. If you need a real response (and CORS), wrap the script in a small Vercel function that proxies to it.
Limitations: Apps Script tops out around 200-500ms per write. The free quota is 90 minutes/day of execution time — fine until you hit ~10,000+ submissions/day. No built-in spam filtering.
Method 2: splitforms webhook → Apps Script
Same Apps Script as above, but with a real form backend in front. You get email notifications, spam filtering, a dashboard, AND your data in Sheets.
Step 1. Sign up at splitforms.com and create an access key.
Step 2. In the splitforms dashboard, add a webhook for that key with the deployment URL:
Webhook URL: https://script.google.com/macros/s/.../exec
Method: POST
Format: JSONStep 3. Point your form at splitforms instead of the script directly:
<form action="https://splitforms.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="hidden" name="redirect" value="https://yoursite.com/thanks" />
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<input name="botcheck" type="checkbox" style="display:none" tabindex="-1" />
<button type="submit">Send</button>
</form>On every submission: splitforms emails you the lead, runs spam filtering, writes a row to your dashboard, AND fires the webhook to Apps Script which appends a row to Sheets. End-to-end latency is ~2 seconds.
Why this beats raw Apps Script: you get spam filtering before the write, retries if Apps Script is having a slow day, email-as-a-fallback when the webhook fails, and a real dashboard to debug from. splitforms' webhook delivery has automatic retries with exponential backoff.
Method 3: n8n self-hosted
n8n is the open-source Zapier alternative. Self-hosted (Docker container, ~$5/mo on a small VPS or free on your home server), it includes a native Google Sheets node with full OAuth and richer operations than Apps Script (upsert, batch write, column-level updates).
The flow:
- Create a Webhook trigger node in n8n. Copy the webhook URL.
- Add a Google Sheets node. Connect via OAuth, choose your sheet, configure "Append" with field mappings.
- Point your form (or splitforms webhook) at the n8n webhook URL.
The Sheets node config (in n8n's JSON export form) looks like:
{
"operation": "append",
"documentId": "YOUR_SHEET_ID",
"sheetName": "Sheet1",
"columns": {
"mappingMode": "defineBelow",
"values": {
"timestamp": "={{$json.timestamp}}",
"name": "={{$json.name}}",
"email": "={{$json.email}}",
"message": "={{$json.message}}"
}
}
}When to use n8n: you have multiple integration destinations (Sheets + Slack + CRM + Mailchimp), need conditional routing, want to enrich data with external API calls before writing to Sheets, or have an internal compliance reason to keep the workflow on your own infra.
When to skip: a single form going to a single Sheet. Apps Script is dead simple in comparison.
Comparison table
Production tips: spam, retries, schema changes
- Always add a honeypot field regardless of method. Without one, Apps Script-only setups will fill your Sheet with bot junk within a week.
- Retry failed writes.Apps Script has rare outages. If you POST directly from the browser, log failures and retry on the next page load. splitforms' webhook delivery handles retries automatically.
- Don't store secrets in the script. The deployment URL is public; anyone who finds it can write to your sheet. Use a shared-secret header check inside
doPostif that matters. - Plan for schema changes.If you add a form field, update the script's
appendRowarray AND add the column header. Otherwise the new data lands in the wrong column. - Watch the 10M cell ceiling. At ~1M rows × 10 columns, Sheets stops accepting writes. Archive to a new sheet monthly or quarterly.
Tech support / troubleshooting
- Apps Script returns 404 even though the URL is right. You did not click Deploy → Manage deployments → Edit → New version after editing the script. The URL is stable but the live code is not until you redeploy.
- CORS error from a browser POST. Apps Script does not set CORS headers. Either use
fetch(url, { mode: 'no-cors' })with FormData (and accept that you cannot read the response), or proxy through splitforms. - Sheet rows show in the wrong column after a schema change. The
appendRowarray is positional — adding a column header is not enough. Update the array to match. - Spam in the sheet within a week. Apps Script alone has no spam protection. Add the honeypot check from the example above, or proxy through splitforms which runs the full layered classifier.
- Hitting the 90-minute daily quota. You crossed roughly 10k+ writes per day. Either move to n8n with the native Sheets node (no quota) or batch-write hourly with a scheduled trigger.
Where to go next and how to get help
- For the splitforms webhook contract, see the docs and API reference.
- Plan limits and security: /faq.
- The full webhooks feature page.
- Want a Slack ping alongside the Sheets row? send form submissions to Slack.
- For other destinations: Notion, Airtable, HubSpot.
Frequently asked questions
How do I send form submissions to Google Sheets without Zapier?
Three free or near-free methods work. (1) Publish a Google Apps Script as a Web App and POST to it directly from your form. (2) Use a form backend like splitforms with a Sheets webhook receiver. (3) Self-host n8n and use its built-in Google Sheets node. Apps Script is the cheapest; splitforms is the easiest; n8n gives you the most flexibility.
Is Google Apps Script free?
Yes — it's bundled with any Google account at no cost. The free tier covers 6 minutes of script runtime per execution and 90 minutes per day total, which is enough for thousands of form submissions per month. The main limitation is execution speed (200-500ms per write to Sheets).
Why not just use Google Forms?
Google Forms works but you lose all design control, get a generic confirmation page, can't easily add custom fields with logic, and there's no way to integrate spam protection or webhooks before the data hits Sheets. For a real product, you want your HTML/React form pointed at a backend that delivers to Sheets.
What's the maximum row count Google Sheets can handle?
Google Sheets caps at 10 million cells per spreadsheet (as of 2026, up from 5M historically). With 10 columns, that's roughly 1 million submissions per sheet — far more than most contact forms ever generate. If you cross that threshold, archive monthly to a new sheet or upgrade to BigQuery.
Can I write to Google Sheets from a static HTML site?
Yes, but indirectly. You can't talk to the Sheets API directly from a browser without exposing OAuth credentials. Either POST to a Google Apps Script Web App (acts as a public proxy) or POST to a form backend like splitforms with a webhook that writes to Sheets server-side.
Does splitforms support Google Sheets natively?
splitforms ships submissions to any webhook URL. Combine it with the Apps Script Web App pattern below and you have a Sheets integration in 10 minutes. We're working on a one-click Sheets integration; for now the webhook path is what we ship.
What if my Apps Script deployment URL stops working after I edit the script?
Apps Script versions are tied to deployment IDs. Editing the script does not redeploy — you have to click Deploy > Manage deployments > Edit > New version. The URL stays the same, but the live code does not update until you do this.
Where do I get help if Sheets writes silently fail?
Open Apps Script's Executions log to see whether doPost ran and whether it threw. If splitforms is the source, the dashboard shows webhook delivery status with response codes. /docs and /api-reference cover the splitforms webhook envelope; /faq covers Sheets-specific rate limits.