Skip to content
ParsedMail

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:

ABCDEFG
email_idreceived_atnameemailphonecompanymessage

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.

  1. 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.
  2. 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.
  3. 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.
  4. Map the id. Put data.id in the email_id column so every row can be traced back to its email.
  5. 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:

OptionHowGood for
Summary rowOne row per email with totals only, items joined into one text cellOverviews and dashboards
One row per itemRepeat the email id and order fields on each item rowPivot tables, product reports
Two tabsOrders tab with one row per email, Items tab with the email id as a keyClean 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 read scope 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.

Frequently asked questions

Can I send emails to Google Sheets without Zapier?

Yes. Use the Apps Script in this guide, which pulls parsed results from the API on a timer, or send the webhook to n8n if you host it yourself.

Why not point the webhook at an Apps Script web app?

Apps Script web apps answer POST requests with a redirect and cannot read request headers. Deliveries would be treated as failed and retried, and the signature could not be checked. Polling the API avoids both problems.

How do I stop duplicate rows?

Write the email id to the first column and check it before adding a row. Duplicate emails within 30 days are already filtered out before parsing.

Can one email create several rows?

Yes. Define a table field such as line items, then loop over its rows in your automation or script and write one row per item.

How fast do rows appear?

With a webhook trigger, usually a few seconds after the email arrives. With the Apps Script timer, within the trigger interval you choose.

More from the blog

See your own email as JSON

Paste one into the demo, describe the fields and look at the result. No account needed.