Lead Capture & Forms

How Do You Get Partial Leads Into Google Sheets Automatically?

Partial leads never trigger a form tool's Sheets integration, because they never submit. Here is the webhook-to-row path that does work, end to end.

Quick answer

Push each lead to the sheet as it is captured, with a webhook — do not export a file later. A tool that records form fields before submit posts one JSON payload per lead; an Apps Script endpoint or an automation platform turns that payload into a row. The sheet updates in seconds and nobody has to remember to download anything. The reason your form tool cannot already do it is that its Google Sheets integration fires on the submit event, and a partial lead never submits.

Recover the leads you're already earning

Tell us what you're trying to track or fix. We'll show you which visitors your forms miss — and how PartialLeads recovers and qualifies them.

Push each lead to the sheet as it is captured, with a webhook, rather than exporting a file later. A tool that records form fields before submit posts one JSON payload per lead; an Apps Script endpoint or an automation platform turns that payload into a row. The sheet updates in seconds and nobody has to remember to download anything.

The spreadsheet is not a reporting choice here. It is where the calling happens — and a lead that arrives tomorrow morning in a CSV is a lead somebody else already called.

Why doesn’t your form tool already do this?

Because it only knows about submitted forms. The native Google Sheets integrations in Typeform, Jotform, Google Forms, Unbounce and most CRMs are wired to the submit event: one submission, one row. No submit, no row. The people you most want in that sheet — the ones who typed an email and left — never trigger it.

This is a structural limit, not a settings problem. Everything those tools know about a visitor arrives in the submit payload, so a half-filled form that is abandoned produces nothing at all. The typed values sit in browser memory and die with the tab.

Partial lead capture changes the trigger. Instead of waiting for a button, a script listens to each field’s input and blur events, waits out a short debounce, and posts the value as it is typed — then flushes anything still pending when the page is hidden or closed. Now there is an event to send to your sheet, because there is a lead before there is a submission.

What has to happen for a lead to reach a sheet on its own?

Four things, all of which have to run without you: capture the fields as they are typed, normalise the email and phone, authenticate to the Google Sheets API, and append a row keyed to something stable so a retry does not write the lead twice.

Capture is the only one of the four that has to happen in the browser.

Normalisation decides whether the sheet is usable. A phone typed as 0412 345 678 and the same phone typed as +61412345678 are two rows to a spreadsheet and one person to a human. Normalise the phone to E.164, and lowercase and trim the email, before either becomes a cell.

Authentication is where no-code plans stall. The Sheets API does not accept a plain HTTP POST from a stranger. You either give a Google service account edit access to the sheet and sign requests as it, or use a platform that already holds an OAuth grant to your Google account.

Appending is one API call, and the only interesting question is idempotency. Carry an identifier for the lead on the row, and write “find this id, update or insert” rather than “add a line” — every retry then converges on one row instead of stacking three.

Diagram of a partial lead moving from field capture through normalisation and a webhook into a Google Sheet row, with the email, phone, source and click ID fields mapped to columns

Which columns should the sheet carry?

One row per lead, never one row per session — mixing the two grains is how sheet totals stop agreeing with the dashboard they came from. Beyond contact details, carry the fields that decide who gets called and the fields that let the row be matched later.

Column Why it is there
lead_id The stable key. Everything else depends on it.
captured_at With an explicit UTC offset. A bare local timestamp starts arguments.
status Partial or completed. Your caller needs to know whether this person finished.
email Lowercased and trimmed at the source, not with a spreadsheet formula.
phone E.164, stored as text so the sheet does not eat the leading +.
last_field The field they reached before leaving. The single best read on intent.
source / medium / campaign As captured. Do not reconstruct them from the landing page later.
click_id gclid, gbraid, wbraid, fbclid, msclkid — whichever arrived.
landing_page The first page of the visit, not the page the form sits on.
country Enough geo to pick a dialling window before somebody calls at 3am.

Two of those get skipped constantly and both cost money. The click ID is what lets the row become a conversion you can import into Google Ads later; without it, the row is a name your platform can never connect to spend. And last_field is free intent data: somebody who stopped at “budget” is a different call from somebody who stopped at “email”.

How do you build this without a developer?

Three routes, in rising order of how well they hold up. All three are buildable in an afternoon by someone comfortable with a settings page.

A scheduled CSV export into the sheet. The floor. It costs nothing and it is stale by design — a lead captured at 09:05 shows up whenever the export runs. Fine for weekly reporting, useless for calling.

An automation platform. Zapier, Make or n8n catch a webhook and run an “add a row to Google Sheets” step. Fastest to build, and where most teams land. Watch two things: they bill per task, so a busy form becomes a monthly line item, and their retry behaviour duplicates rows unless the step is an update-or-insert against your lead_id.

A Google Apps Script web app. A doPost endpoint bound to the sheet, deployed as “anyone with the link,” with a shared secret checked on every request. It costs nothing, holds its own permission to the sheet, and gives you the find-or-update logic the no-code steps make awkward:

function doPost(e) {
  const body = JSON.parse(e.postData.contents);
  if (body.secret !== PropertiesService.getScriptProperties().getProperty('SECRET')) {
    return ContentService.createTextOutput('forbidden');
  }

  const sheet = SpreadsheetApp.getActive().getSheetByName('Leads');
  const ids = sheet.getRange('A2:A').getValues().flat();
  const row = [
    body.lead_id, body.captured_at, body.status, body.email,
    "'" + body.phone,            // leading quote keeps E.164 as text
    body.last_field, body.source, body.campaign, body.click_id, body.landing_page
  ];

  const found = ids.indexOf(body.lead_id);   // find-or-update, so retries converge
  if (found > -1) {
    sheet.getRange(found + 2, 1, 1, row.length).setValues([row]);
  } else {
    sheet.appendRow(row);
  }
  return ContentService.createTextOutput('ok');
}

Deploy that, paste the URL into your capture tool’s webhook field, and the sheet fills itself. The trade-off is ownership: redeploying after an edit is manual, and Apps Script runs under Google’s execution quotas, so a very high-volume form eventually wants a real backend.

Why do rows duplicate, and how do you stop it?

Three separate causes, needing three different fixes. Deduplicating with a spreadsheet formula treats all three as one problem, which is why it never quite works.

Delivery retries. A webhook that times out gets sent again, and a plain appendRow writes the lead twice. Fixed by the find-or-update pattern above, keyed on the lead id.

The partial-to-completed upgrade. The same person is captured mid-form and then submits, so you hold two events for one lead. That is not a duplicate — it is a status change, and the second event should overwrite the row.

One person, several sessions. Somebody starts the form on a phone at lunch and finishes on a laptop that night. Two visitors, two form starts, one human. No spreadsheet formula resolves that, because the rows may share no exact field: a work email on one, a personal email on the other. It is fixed upstream, by identity resolution that joins sessions on email, phone, click ID and device before anything is dispatched — the same reason one customer so often counts as three leads.

Can that same sheet feed Google Ads offline conversions?

Yes, and it is the reason to insist on the click ID column. Google Ads can be pointed at a Google Sheet on a schedule and pull conversion rows from it, turning your call list into the feedback loop Smart Bidding runs on.

What the rows need is narrow: a click identifier, the conversion action name exactly as it is spelled in your account, a conversion time with an explicit timezone, and — when you have it — a value and a currency. Use Google’s own template sheet for the column headers rather than typing them from memory, because the import matches on header text and fails silently against a near-miss.

Click IDs are not always a gclid any more: app and iOS campaigns hand you gbraid and wbraid instead, and a row carrying the wrong one matches nothing. Where no click ID survived, the fallback is hashed email and phone rather than the raw values — the other reason to normalise before the row is written.

How does PartialLeads get partial leads into Google Sheets?

By making capture and dispatch the same pipeline. Fields are captured on input and blur with a short debounce and flushed before the page unloads, so a visitor who types an email and leaves already exists as a lead. That lead is normalised server-side — phone to E.164 with a country fallback from the session’s geo, email lowercased and trimmed, Gmail dot and plus variants collapsed before hashing — then handed to whichever outbound path you have switched on.

For Google, that path is a scheduled Google Sheets upload: PartialLeads writes conversion rows carrying gclid, gbraid or wbraid plus hashed email and phone, and Google Ads imports them on its own schedule. For everything else, the same fan-out posts the lead to your own webhook endpoint — the Apps Script above, your automation platform, or your CRM — with the attribution attached.

The two paths are not interchangeable. The Sheets dispatcher exists to feed Google Ads offline conversions, so its rows carry click identifiers and hashed contact fields, not your full captured-field set. If what you want is a call sheet with every field on it, the webhook is the route, and the sheet on the other end stays yours to shape. A lead who typed an email and never clicked submit can travel either way.

PartialLeads Leads list showing partial and completed leads with source badges, journey ribbons and the API column confirming which conversion platforms each lead was dispatched to

The proving surface is the Leads list. Each lead carries a Partial or Completed badge, its journey rendered left to right as source badges, and an API column showing which conversion platforms that lead was dispatched to — so a lead that never reached your sheet is visible as a row, not as an absence. Unique-contact counts sit beside the raw lead count, which is where a duplicate problem shows up before it reaches the spreadsheet.

The honest constraints: dispatch is the half anyone controls, and matching is not. Google decides whether an uploaded conversion ties back to a click, and a row with neither a surviving click ID nor a resolvable hashed identifier will not match. Nothing here makes a person answer the phone either — the sheet is only worth building if somebody actually works it.

What breaks The mechanism Where you see it in the dashboard
The form tool’s Sheets integration only fires on submit Field capture on input/blur, flushed before the page unloads Leads list, Partial badge
Phone numbers arrive in six formats and the sheet treats them as six people E.164 normalisation with a country fallback from session geo Phone column on the lead
Gmail dot and plus variants split one person across rows Dot and plus normalisation applied before hashing Unique contacts beside the lead count
The same person fills the form on a phone and a laptop Six-tier identity cluster: visitor ID, email, phone, IP and user agent, device, click ID Journey ribbon on the Leads list
The row reaches Google Ads with nothing to match on gclid, gbraid and wbraid captured at first touch and carried onto the upload row with hashed email and phone Google Sheets conversion rows
Nobody can tell whether a lead was ever sent anywhere Per-lead dispatch record of the platforms the lead went to API column on the Leads list

Recover the leads you're already earning

Tell us what you're trying to track or fix. We'll show you which visitors your forms miss — and how PartialLeads recovers and qualifies them.

Sources


Frequently asked questions

QCan Google Sheets receive a webhook directly?
No. A spreadsheet has no inbound URL of its own, so something has to sit in front of it. The two usual options are a Google Apps Script web app deployed from the sheet itself, which gives you a URL and runs with permission to write, or an automation platform that catches the webhook and calls the Sheets API on your behalf. Either way, a middle layer exists.
QWill Zapier work for getting partial leads into a sheet?
Yes, with two caveats. It bills per task, so a form that captures hundreds of partial leads a week becomes a real monthly cost, and its retries will duplicate rows unless the step is configured to find and update an existing row rather than append a new one. For low volume it is the fastest thing to set up. For high volume, a small script is cheaper and more predictable.
QHow many rows can a Google Sheet hold before it becomes a problem?
Sheets have a documented cell ceiling, but you will feel the slowdown long before you reach it — filters, formulas and conditional formatting across tens of thousands of rows make the tab painful to open. Treat the sheet as a working queue rather than an archive: keep the current period live, move older rows to a monthly tab or export them, and keep formula columns to a minimum.
QIs it legal to put captured-but-unsubmitted emails in a spreadsheet?
It depends on your jurisdiction and your consent setup, and it is worth an actual answer from someone qualified rather than a blog's. The parts that are always true: you need a lawful basis for processing, your privacy notice has to describe what the form collects and when, the spreadsheet needs access control like any other store of personal data, and a deletion request has to reach the sheet too — which is a good argument for one sheet rather than five copies.
QCan I send partial leads to Excel or Airtable instead?
Yes, and the shape of the work is identical. The webhook does not care what is on the other end — Airtable takes a POST to its API directly, and Excel via Microsoft's Power Automate or a file in OneDrive. Google Sheets gets recommended most because Apps Script gives you a free endpoint bound to the sheet itself, with no third platform in the path.
QDo partial leads work as Google Ads conversions?
They can, and the usual reason to do it is conversion volume — a campaign starved of conversion signal bids badly. Two rules keep it honest. Import them as their own conversion action rather than mixing them with submitted leads, and set that action to "secondary" unless you genuinely want bidding optimised toward half-finished forms. Otherwise you teach the algorithm to buy abandonment.
QWhat if the same person appears twice with different email addresses?
A spreadsheet cannot resolve that, because the two rows share no matching field. It is solved before the row is written, by joining sessions on the identifiers they do share — a phone number, a device, a click ID, a visitor cookie — and treating the result as one contact with two emails. If your rows arrive already deduplicated, the sheet stays clean. If they do not, no formula will save it.
QDo I need a developer to keep this running?
Not to build it, but plan for the two things that break unattended. Column order changes silently break any script that writes by position, so write by header name if you can. And an Apps Script deployment needs redeploying after an edit, which is the most common reason a sheet that worked for months stops filling. Check that rows are still arriving weekly rather than discovering it on a quiet Monday.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.