Parse email to Google Sheets
To parse email to Google Sheets, forward the emails to a parsing inbox that extracts the fields you need, then append one row per email to the sheet. Use Zapier, Make or n8n with a webhook trigger for a no-code setup, or a short Apps Script that pulls parsed results from the API.
Start with the sheet you want to end up with
The most common mistake is to start with the emails. Start with the spreadsheet. Decide the columns, their order and their format, then make the parser produce exactly those fields.
A lead sheet, for example:
| A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|
| email_id | received_at | name | phone | company | message |
Two columns deserve a note. email_id is the parser's id for the email (eml_...), which you use to avoid duplicate rows. received_at comes from the parser, not from the time the row was written, so delayed rows still sort correctly.
In ParsedMail, describe the business columns in plain English ("name, email, phone, company, message") and the schema produces typed fields: email for the address, phone for the number, string for the rest. The email to spreadsheet use case shows more layouts.
Path one with Zapier, Make or n8n
This path needs no code and takes about fifteen minutes. The parsing inbox sends JSON to a webhook URL that your automation tool provides, and the tool appends the row.
- Create the trigger. In Zapier use "Webhooks by Zapier" with the Catch Hook trigger. In Make use "Webhooks" and "Custom webhook". In n8n add a "Webhook" node set to POST. Copy the URL each one gives you.
- Add the URL as an endpoint in ParsedMail, limited to the inbox you want. Send a test event so the automation tool learns the structure.
- Add the Google Sheets step. "Create Spreadsheet Row" in Zapier, "Add a Row" in Make, "Append Row" in n8n. Choose the sheet and map each column to a field under
data.data. - Map the id. Put
data.idin the email_id column so every row can be traced back to its email. - Forward a real email and check the row.
ParsedMail has no native app in these marketplaces, so it uses the generic webhook trigger each tool already has. The integrations page describes each setup. If your automation needs only certain emails, add a filter step on data.needs_review or data.confidence_avg.
Path two with Apps Script and the API
If you prefer to keep everything inside Google Workspace, an Apps Script bound to the sheet can pull parsed emails from the REST API on a timer. You might expect to point the webhook straight at an Apps Script web app instead. That route has two problems. Apps Script web apps answer POST requests with a redirect, and ParsedMail does not follow redirects, so deliveries count as failed and get retried. And doPost cannot read request headers, so you cannot verify the signature. Polling avoids both.
const API = "https://parsedmail.com/api/v1";
const INBOX = "inb_YOUR_INBOX_ID";
const COLUMNS = ["name", "email", "phone", "company", "message"];
function pullParsedEmails() {
const key = PropertiesService.getScriptProperties().getProperty("PARSEDMAIL_KEY");
const props = PropertiesService.getScriptProperties();
const since = props.getProperty("SINCE") || new Date(Date.now() - 864e5).toISOString();
const sheet = SpreadsheetApp.getActive().getSheetByName("Leads");
const seen = new Set(sheet.getRange("A:A").getValues().flat().filter(String));
let after = null, newest = since, rows = [];
do {
let url = `${API}/emails?inbox=${INBOX}&since=${encodeURIComponent(since)}&limit=100`;
if (after) url += `&starting_after=${after}`;
const res = UrlFetchApp.fetch(url, { headers: { Authorization: `Bearer ${key}` } });
const page = JSON.parse(res.getContentText());
for (const e of page.data) {
after = e.id;
if (e.received_at > newest) newest = e.received_at;
if (!["parsed", "delivered"].includes(e.status) || seen.has(e.id)) continue;
rows.push([e.id, e.received_at, ...COLUMNS.map(c => e.data[c] ?? "")]);
seen.add(e.id);
}
if (!page.has_more) break;
} while (true);
if (rows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
}
props.setProperty("SINCE", newest);
}
Store the API key in Script Properties, not in the code. Create a key with only the read scope on the API keys page. Then add a time-driven trigger (Triggers, Add trigger, every 5 minutes). The script writes all new rows in one setValues call, which is much faster than appending row by row. The endpoints are described on the email parsing API page.
Keeping exactly one row per email
Duplicate rows are the main complaint about email to spreadsheet setups. They come from three places, and each has its own fix:
- The same email arrives twice. A forward plus a mail rule, or a sender who resends. ParsedMail marks a second copy within 30 days (same Message-ID, or same sender, subject and body) as
duplicate. It is not parsed and not sent on, so no extra row appears. - The same delivery is retried. If the automation tool answered slowly or with an error, the webhook is retried. Zapier and Make keep running the steps on each retry. Add a "Lookup Spreadsheet Row" step on the email_id column before the create step, and stop when it finds a match.
- The script runs twice over the same window. The Apps Script above reads column A and skips ids it already wrote, so overlapping runs do not duplicate rows.
The common thread is the email id in the first column. Keep it even if nobody reads it.
Line items and other repeating data
An order email with three products does not fit into one row neatly. You have three options:
| Option | How | Good for |
|---|---|---|
| Summary row | One row per email with totals only, items joined into one text cell | Overviews and dashboards |
| One row per item | Repeat the email id and order fields on each item row | Pivot tables, product reports |
| Two tabs | Orders tab with one row per email, Items tab with the email id as a key | Clean data that grows over time |
Define line items as a table field in the schema, for example "products with name, quantity and price". The result is an array of row objects. In Zapier and Make you can loop over that array to write one row per item. In Apps Script, use e.data.products.map(...) to build the item rows.
Formatting values so the sheet stays usable
Typed output saves most of the cleanup that usually follows email to spreadsheet setups:
- Dates arrive as
YYYY-MM-DD, which Google Sheets recognizes as a date in every locale. - Money arrives as an object, for example
{"amount": 189.0, "currency": "USD"}. Write the amount and the currency to two columns, so you can sum the amounts. - Missing values arrive as
null. Write an empty cell, not the word null. - Phone numbers can lose a leading plus or zero if Sheets treats them as numbers. Format the column as plain text first.
Consider a confidence column too. Writing confidence_avg lets you sort or filter rows that deserve a second look, and conditional formatting can color anything below 0.8.
Sharing the sheet without leaking data
Emails that end up in a spreadsheet often contain names, phone numbers and addresses. A sheet is easy to share by accident, so set a few rules before the first row arrives:
- Keep the sheet in a shared drive owned by the team, not in one person's drive, so access does not depend on one account.
- Share with named people or groups. Avoid "anyone with the link".
- Only write the columns people need. If the sales team needs name and company, there is no reason to copy the full message body.
- Use a dedicated automation account or API key with the narrowest scope. For the Apps Script path, a key with only the
readscope cannot change anything in ParsedMail. - Decide how long rows stay. A monthly script that removes rows older than a set age keeps the sheet small and limits what is exposed.
On the parsing side, raw emails are kept for a limited time that depends on the plan (30 days on Starter, 90 on Growth, configurable on Scale) and are never used for model training. The extracted JSON stays available through the API.
When a spreadsheet stops being the right place
Google Sheets works well for leads, bookings, small order volumes and anything a team reviews by eye. A sheet has a cell limit per spreadsheet, slows down with heavy formulas, and has no locking for concurrent writers. If you expect tens of thousands of rows a month, or several automations writing into the same sheet, send the same JSON into a database or Airtable instead and keep the sheet as a view on top. The parsing setup does not change. Only the last step moves.
To see how a sample email becomes a row before you build anything, try the free demo of the email parser on the homepage.