# Google Sheets Import & Export: Sync, Upsert, Dedupe (`charlesmarkette/google-sheets-sync`) Actor

Export any Apify dataset to Google Sheets after every scraper run: append, replace or upsert rows by key, skip duplicates, and add new fields as columns. Or import a sheet into a dataset. Uses your own Google service account, so there's no login to expire. $0.01 per 1,000 rows.

- **URL**: https://apify.com/charlesmarkette/google-sheets-sync.md
- **Developed by:** [Charles Markette](https://apify.com/charlesmarkette) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / 1,000 row written or reads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

**Google Sheets Sync** exports any Apify dataset to **Google Sheets**, or imports a sheet back into a dataset. Run it on its own, or connect it as the last step after any scraper so every run **appends, replaces or upserts** rows in your spreadsheet automatically. It's for anyone who keeps scraped leads, prices, listings or monitoring results in a Google Sheet: marketers, sales teams, analysts and no-code builders, as well as developers who want a reliable Sheets export from the Apify API.

- **Append:** add rows to the end. Optionally skip rows whose key, for example `url` or `id`, is already in the sheet (dedupe on append).
- **Replace:** rewrite the tab with the new data. The grid is sized exactly, and old rows and columns are removed.
- **Upsert:** update rows that have the same key, add the new ones, and leave every other row alone. This is ideal for price trackers, lead lists and daily monitors.
- **Read (import):** load a tab into an Apify dataset. The first row is the header.

### Why use Google Sheets Sync?

- **Your own Google service account.** There's no Google login to expire or re-connect, and changes to Actor permissions don't break your schedules. The key is a secret input, stored encrypted.
- **Safe big writes.** Rows go out in large batches with automatic backoff on Google's per-minute quota. Google's 10M-cell limit (all tabs, whole grid) is checked before anything is written. Appends use retry-safe writes below your last used row, so a retried request can't add the same rows twice.
- **Your sheet stays intact.** In upsert mode, rows you didn't send keep their formulas, dates and exact text (`00123` stays `00123`), and columns with blank or repeated headers keep their data. Very large IDs are written as text so they never get rounded, and in the default RAW mode, scraped text that starts with `=` stays text instead of becoming a formula (also when an upsert rewrites it later).
- **New fields become new columns automatically,** and existing columns keep their place. Nested data is flattened into columns like `address/city` and `images/0`.
- **Limited permissions.** The Actor can only read the dataset you pick and its own storage.
- **Low, predictable price.** $0.01 per 1,000 rows; duplicates you skip are free.

### Use cases

- **Scraper to spreadsheet:** send Google Maps, Amazon, LinkedIn or any other scraper's results to a sheet after every run.
- **Price and stock trackers:** upsert by product URL or SKU so each product stays on one row and its price updates daily.
- **Lead lists:** append new leads and skip the ones already in the sheet, so your sales team never works a lead twice.
- **Monitoring dashboards:** keep a sheet that Looker Studio or a pivot table reads from up to date.
- **Import a sheet into Apify:** read a tab of URLs or keywords into a dataset that another Actor uses as input.

### How to use Google Sheets Sync

#### Setup (about 3 minutes, one time)

1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project, or pick an existing one.
2. Enable the **Google Sheets API**: APIs & Services → Library → Google Sheets API → Enable.
3. Go to **IAM & Admin → Service Accounts → Create service account**. Give it any name, and skip the roles.
4. Open the service account, then **Keys → Add key → Create new key → JSON**. A `.json` file downloads.
5. Open your spreadsheet and click **Share**. Add the service account's `client_email` from the JSON file (it looks like `name@project.iam.gserviceaccount.com`) as an **Editor**.
6. Paste the whole JSON file content into **Google service account key**, and paste the spreadsheet URL.

#### Export a dataset to Google Sheets

1. Pick the **Dataset** to export (for example the output of a scraper run).
2. Choose the **Mode** (`append`, `replace` or `upsert`) and, for upsert or dedupe, the **Key fields** (like `url`).
3. Click **Start**. The run summary in the key-value store `OUTPUT` tells you how many rows were written, updated, added and skipped, with a link to the spreadsheet.

#### Run it automatically after another Actor (integration)

1. Open the scraper's Actor or saved task, go to the **Integrations** tab and click **Add integration**.
2. Search for **Google Sheets Sync**, choose to start it when the run **succeeded**, and set the input. Apify fills in `{{resource.defaultDatasetId}}` with that run's dataset:

```json
{
  "spreadsheet": "https://docs.google.com/spreadsheets/d/…",
  "sheetName": "Leads",
  "mode": "upsert",
  "keyFields": ["url"],
  "datasetId": "{{resource.defaultDatasetId}}",
  "serviceAccountKey": "…"
}
```

3. Save, and use the integration's test menu to try it on a past run.

Only want the items that are new since the last run? Put [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) in "only new" mode in between, or use upsert / append with **Skip rows already in the sheet**.

### Input example

```json
{
  "spreadsheet": "https://docs.google.com/spreadsheets/d/1AbC…/edit",
  "sheetName": "Prices",
  "mode": "append",
  "datasetId": "aBcDeFgHiJkLmNoPq",
  "keyFields": ["url"],
  "dedupeOnAppend": true,
  "columns": ["title", "price", "url"],
  "valueInputOption": "RAW",
  "serviceAccountKey": "{ \"type\": \"service_account\", … }"
}
```

| Field | Notes |
|---|---|
| Spreadsheet URL or ID | The sheet must be shared with the service account as Editor. Without a spreadsheet and key, the run only returns a setup reminder (nothing is written). |
| Sheet (tab) name | Created if it doesn't exist (write modes). Read mode fails if the tab is missing. Default `Sheet1`. |
| Mode | `append`, `replace`, `upsert` or `read`. |
| Dataset | The dataset to export. In integrations use `{{resource.defaultDatasetId}}`. |
| Raw data | Alternative to a dataset: objects, or arrays with a header row. |
| Key fields | Identify a row for upsert and for skipping duplicates. |
| Skip rows already in the sheet | Append only. |
| Columns | Optional field list and order. |
| How Google should interpret values | `RAW` (exact values) or `USER_ENTERED` (formulas, dates and locale parsing). |
| Nested field separator | Default `/`, as in `address/city`. |
| Max rows | Optional cap. |

### Output example

**Write modes** save a run summary in the key-value store `OUTPUT`:

```json
{
  "mode": "upsert",
  "spreadsheetId": "1AbC…",
  "sheet": "Prices",
  "rowsWritten": 2140,
  "rowsRead": 0,
  "updated": 1985,
  "added": 15,
  "skippedDuplicates": 0,
  "columns": ["url", "title", "price", "currency", "inStock"],
  "apiRequests": 6,
  "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1AbC…"
}
```

In upsert mode `rowsWritten` is the size of the whole tab after the merge, and `updated` + `added` are the rows you sent. **Read mode** outputs one dataset item per sheet row, with the header row as field names (blank headers become `column_N`, repeated ones `name_N`).

### How much does it cost to export to Google Sheets?

Google Sheets Sync uses pay-per-event pricing: **$0.00001 per row written or read** ($0.01 per 1,000 rows), plus a $0.001 run start fee. Rows are charged as they're written. In upsert mode you pay for the rows you sent that were updated or added (each key once); rows already in the sheet that you didn't send are free. Rows skipped as duplicates are free.

*Examples use the Free-plan rate. Where an example doesn't add it, each run also has its run-start charge, counted per GB of Actor memory (minimum one; one at the default memory); Apify Proxy (where used) is billed separately.*

**Worked example:** a daily price tracker that upserts 2,000 products costs 2,000 × $0.00001 = $0.02, plus $0.001, so **$0.021 per run**, or about **$0.63 per month** with 30 daily runs.

If you set a maximum cost per run, the Actor writes only as many rows as that allows and tells you in the log.

### FAQ

#### Do I need to connect my Google account?

No. You use a Google Cloud **service account** (a robot account) and share the spreadsheet with its email address. Nothing expires, so scheduled runs keep working.

#### "Google denied access (403)". What's wrong?

The spreadsheet isn't shared with the service account's `client_email` as an **Editor**, or the Google Sheets API isn't enabled in the service account's Cloud project. Fix either and run again.

#### "Spreadsheet not found (404)".

Check the URL or ID, and that the spreadsheet is shared with the service account.

#### "Service account key must be the full JSON key file content".

Paste the whole downloaded `.json` file, from the first `{` to the last `}`, without editing it. If Google says the key was rejected, it may have been deleted or disabled: create a new JSON key.

#### How does upsert decide which row to update?

By the **Key fields**. Key matching treats `1`, `1.0` and `"1"` as the same key. Rows with an empty key are never matched (blank rows stay where they are), and input rows with an empty key are added as new rows.

#### My dates or numbers look different in the sheet.

In the default `RAW` mode values are stored exactly as given. Choose `USER_ENTERED` if you want Google to parse dates, numbers and `=` formulas in your data the way it does when you type them. Cells already in the sheet keep their formulas, dates and text either way.

#### The run says the sheet would go over 10 million cells.

Google counts every cell of every tab's grid. Use **Columns** to pick fewer fields, delete unused rows and columns in other tabs, or write to another spreadsheet. Nothing is written when this check fails.

#### Can I write to several tabs?

Yes, one tab per run. Save one task per tab, or connect several integrations.

### Limitations

- Google allows 10 million cells per spreadsheet and 50,000 characters per cell. Longer values are cut off with "…".
- Upsert and replace rewrite the tab's values and size the grid to fit the data, so extra empty rows and columns are removed. Cell formatting is kept.
- Memory: at the default 1 GB, a run handles about 2 million cells (rows × columns). Give the run more memory for bigger sheets.
- If a run fails partway through a large write (for example Google is down for minutes), the rows written so far stay and are billed; re-run the same input in upsert mode, or append with "Skip rows already in the sheet", to finish without duplicates.
- Use one writer per tab: two runs appending or upserting into the same tab at the same moment can overwrite each other's rows. Give parallel scrapers their own tabs.
- Append mode reads the tab once to find the last used row (and existing keys when skipping duplicates).
- Not affiliated with Google.

### Related Actors

Google Sheets Sync is part of a small suite of Actors that work together after any scraper:

| Actor | Use it to |
|---|---|
| [CSV & Excel to Dataset](https://apify.com/charlesmarkette/csv-excel-to-dataset-xlsx-json-converter) | Read a shared Google Sheet or an Excel/CSV file back into a dataset. |
| [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) | Merge runs and keep only items not seen before, then send just those to your sheet. |
| [Send Email Report & Slack, Discord, Teams, Telegram Alerts](https://apify.com/charlesmarkette/send-email-report-slack-discord-teams-telegram-alerts) | Get a Slack or email message with a preview whenever new rows arrive. |
| [Dataset Delivery](https://apify.com/charlesmarkette/dataset-delivery-export-to-sftp-ftp-dropbox-google-drive) | Deliver the same data as a CSV or Excel file to SFTP, FTP, Dropbox or Google Drive. |
| [SEC Funding Rounds Tracker](https://apify.com/charlesmarkette/sec-form-d-funding-tracker) | Feed a sheet of newly funded companies from SEC Form D, Form C and Reg A filings. |
| [Vector DB Sync](https://apify.com/charlesmarkette/vector-db-sync-pinecone-qdrant-rag-loader-delta-updates) | Load any dataset into Pinecone or Qdrant for RAG. |
| [Phone Number Validator & Formatter](https://apify.com/charlesmarkette/phone-number-validator-formatter-bulk-e164) | Validate and format phone numbers (E.164) before they go into your sheet. |
| [Dataset Filter & Transform](https://apify.com/charlesmarkette/dataset-filter-transform-rename-flatten-fields) | Pick, rename and flatten fields so they fit your sheet's columns. |
| [RSS & Atom Feed Reader](https://apify.com/charlesmarkette/rss-atom-feed-reader-only-new-items) | Feed new blog, news or podcast items into a sheet on a schedule. |
| [Bulk URL Status Checker](https://apify.com/charlesmarkette/bulk-url-status-checker-redirects-ssl) | Check a sheet of URLs for broken links and SSL expiry on a schedule. |
| [PDF & Image to Text](https://apify.com/charlesmarkette/pdf-image-to-text-markdown-ocr) | Pull tables and text out of PDFs and scans, then send the rows to your sheet. |
| [Broken Link Checker](https://apify.com/charlesmarkette/broken-link-checker-crawl-website-404) | Crawl a website for broken links and keep the list in a Google Sheet. |
| [Website Contact Info Scraper](https://apify.com/charlesmarkette/website-contact-info-scraper-emails-phones-socials) | Collect company contact details from their websites and send them to your sheet. |
| [Website Tech Stack Detector](https://apify.com/charlesmarkette/website-tech-stack-detector-shopify-cms-analytics) | Keep a sheet of websites with their platform and marketing tools. |
| [Career Page Jobs Scraper](https://apify.com/charlesmarkette/career-page-jobs-scraper-greenhouse-lever-ashby) | Keep a sheet of open jobs at the companies you track. |

# Actor input Schema

## `spreadsheet` (type: `string`):

The Google Sheets URL (or its ID). Share the spreadsheet with your service account's client_email as an Editor. Required to read or write a sheet.

## `sheetName` (type: `string`):

The tab to write to or read from. Created automatically if it doesn't exist.

## `mode` (type: `string`):

What to do with the sheet.

## `datasetId` (type: `string`):

The Apify dataset to export (e.g. the output of another Actor run). In an integration, use {{resource.defaultDatasetId}}. Not needed for Read mode.

## `serviceAccountKey` (type: `string`):

Paste the whole JSON key of a Google Cloud service account with the Google Sheets API enabled. It is stored encrypted. See the README for the 3-minute setup. Required to read or write a sheet.

## `keyFields` (type: `array`):

Field(s) that identify a row, e.g. 'url' or 'id'. Required for Upsert and for 'Skip rows already in the sheet'.

## `dedupeOnAppend` (type: `boolean`):

In Append mode, don't add rows whose key fields already exist in the sheet (or appear earlier in this run).

## `columns` (type: `array`):

Optional. Only these fields, in this order (after any columns already in the sheet). Nested fields are flattened like 'address/city'. Empty = all fields.

## `rawData` (type: `array`):

Optional. An array of objects, or an array of arrays whose first row is the header.

## `valueInputOption` (type: `string`):

RAW stores text exactly as given (numbers and true/false stay numbers and booleans). USER_ENTERED lets Google parse '=SUM(...)' formulas and locale-specific dates and numbers in your data. Either way, cells already in the sheet keep their formulas, dates and text.

## `flattenSeparator` (type: `string`):

Separator used when flattening nested objects and arrays into columns.

## `maxRows` (type: `integer`):

Optional cap on rows written (or read). In append mode it counts rows after duplicates are skipped; in upsert mode it counts distinct keys.

## Actor input object example

```json
{
  "sheetName": "Sheet1",
  "mode": "append",
  "dedupeOnAppend": false,
  "rawData": [],
  "valueInputOption": "RAW",
  "flattenSeparator": "/"
}
```

# Actor output Schema

## `summary` (type: `string`):

Rows written/updated/added, columns and the spreadsheet link.

## `rows` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "rawData": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("charlesmarkette/google-sheets-sync").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "rawData": [] }

# Run the Actor and wait for it to finish
run = client.actor("charlesmarkette/google-sheets-sync").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "rawData": []
}' |
apify call charlesmarkette/google-sheets-sync --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,charlesmarkette/google-sheets-sync"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/FqSIayRxoTvTVHko0/builds/XIuuUxgxTcZNlXY9q/openapi.json
