# Sheets Writer - No Google Verification Needed (`kasimba/sheets-writer`) Actor

Write any Apify dataset or JSON array to a Google Sheet using a service account - no OAuth consent screen, no Google app verification, no token that expires after a few days. Share your sheet with the service account email once; every write after that just works.

- **URL**: https://apify.com/kasimba/sheets-writer.md
- **Developed by:** [John J. K. Omolo](https://apify.com/kasimba) (community)
- **Categories:** Automation, Integrations
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 sheet writes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Sheets Writer — No Google Verification Needed

Write any Apify dataset — including this catalogue's own Actors — or a raw JSON array straight into a Google Sheet.

### Why this exists

The established alternative in this niche uses OAuth consent — the "Connect Google Account" flow. Roughly a third of its closed issues, spanning 2–4 years against an actively maintained project, are the same two things: *"App blocked by Google"* and *"Error invalid\_grant after a few days."* Both are confirmed signatures of an OAuth app that hasn't completed Google's formal verification for sensitive scopes — the unverified-app warning screen, and the short refresh-token lifetime Google applies to unverified apps in testing mode.

**This Actor uses a service account instead.** Confirmed directly from Google's own documentation: an app that only accesses data explicitly shared with it via a service account is exempt from the verification requirement entirely — not a faster path through review, a different door with no review. Neither failure mode above is structurally possible here.

The real trade-off, stated plainly: setup takes one extra manual step. You share your spreadsheet with the service account's email address, the same way you'd share it with a person, rather than clicking through a one-click OAuth prompt. It's the same pattern Zapier and Make.com use, for the same reason.

### Setup

1. In Google Cloud Console, create a service account (**IAM & Admin → Service Accounts**) and download its JSON key.
2. Paste the full contents of that key file into `serviceAccountKey`.
3. Open your target spreadsheet, click **Share**, and add the service account's email address (the `client_email` field in the key file) as an **Editor**.

That's it — no OAuth screen, nothing to submit to Google for review.

### Input

| Field | Required | Description |
|---|---|---|
| `serviceAccountKey` | yes | The full JSON key file contents, pasted as text. Stored encrypted. |
| `spreadsheetId` | yes | The long ID in the sheet's URL, between `/d/` and `/edit`. |
| `sheetName` | no | Which tab to write to. Default `Sheet1`. |
| `datasetId` | one of these two | An existing Apify dataset — including the output of any other Actor, such as this catalogue's own. |
| `data` | one of these two | A raw JSON array of objects. |

### Pricing

Billed per record actually written, not per run — writing 2 rows and writing 20,000 rows are not the same amount of value delivered. A run that writes nothing is never billed.

### What "write" means, honestly

**v1 appends.** Records go to the next empty row; nothing existing is overwritten.

**Columns are the union of every field across every record**, in the order first encountered — not just the first record's keys. This matters for real scraped data, where records are rarely uniform: in a batch of job listings where the first one happens to disclose no salary, the salary columns still appear, and the listings that do disclose still land in them.

**Writing to a sheet that already has a header row appends underneath it**, using that row's existing column order. No second header is stamped mid-table, and values can't drift under the wrong headings between runs with different fields. Any field absent from that existing header isn't written — it's named in the run log and in `unmappedKeys` on the output, so it's visible rather than silent. To capture new fields, add them to row 1 or write to an empty sheet.

**Values are written exactly as given.** Google's Sheets API can optionally parse each value as though a human typed it into the cell; this Actor deliberately doesn't. That parsing strips leading zeros from `007`, turns `1-2` into a date, converts long numeric IDs to scientific notation, and evaluates a phone number like `+1-555-0100` as arithmetic. Numbers and booleans still arrive as real numbers and booleans, because they're typed in the source data.

A nested value (a list, a dict — something a scraper Actor could legitimately produce, like a findings array) is written as its JSON text rather than dropped, since the Sheets API only accepts flat values. An *empty* list or dict is written as a blank cell, matching how a missing field looks.

### The most common real failure, and what happens when it hits

**"Permission denied"** almost always means the spreadsheet hasn't been shared with the service account yet. This Actor's error message names the exact email to share it with — pulled from your own key file — rather than a bare API error code. That diagnosability is the actual point of this Actor, not a nice-to-have: the incumbent's most common complaints are exactly the kind of unhelpful platform error this is built to translate into an instruction you can act on immediately.

### For maintainers

The request shapes (`values.append`, `valueInputOption`, the `majorDimension`/`values` body) are confirmed against Google's official REST reference, not the SDK's dynamic discovery surface — this Actor calls the REST API directly via `google-auth`'s `AuthorizedSession` rather than the full `googleapiclient` package, avoiding a network-fetched discovery document on every run. Credential parsing, row-building, and error-message formatting are all tested against synthetic data.

`values.append` is confirmed live end to end: real service account, real shared sheet, correct header row, correct append-not-overwrite. The column and value-parsing behaviour described above was also established live rather than assumed — the earlier build derived columns from the first record alone and dropped fields silently, and wrote with `USER_ENTERED`, which was observed turning `+1-555-0100` into `-654`.

**What isn't yet verified:** the `values.get` call that reads an existing header row is newer than that live run, as is the 400-handling path for a mistyped tab name. Both are reasoned from the same REST reference, not yet observed against Google.

# Actor input Schema

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

Paste the full contents of your downloaded service account key .json file - not a file path, the whole file.

## `spreadsheetId` (type: `string`):

The long ID in the sheet's URL, between /d/ and /edit - not the sheet's name.

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

Which tab to write to.

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

An existing Apify dataset to write - including output from another Actor. Leave blank if using `data` instead.

## `data` (type: `array`):

A JSON array of objects to write. Leave blank if using `datasetId`.

## Actor input object example

```json
{
  "sheetName": "Sheet1"
}
```

# Actor output Schema

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

Records written, columns, and a direct link to the spreadsheet.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("kasimba/sheets-writer").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("kasimba/sheets-writer").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 '{}' |
apify call kasimba/sheets-writer --silent --output-dataset

```

## MCP server setup

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

```

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/7vcfTjVbZlWgoqHA1/builds/J3f0FN8X8INXbaZgC/openapi.json
