Sheets Writer - No Google Verification Needed
Pricing
from $1.05 / 1,000 sheet writes
Sheets Writer - No Google Verification Needed
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.
Pricing
from $1.05 / 1,000 sheet writes
Rating
0.0
(0)
Developer
John J. K. Omolo
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
5 days ago
Last modified
Categories
Share
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
- In Google Cloud Console, create a service account (IAM & Admin → Service Accounts) and download its JSON key.
- Paste the full contents of that key file into
serviceAccountKey. - Open your target spreadsheet, click Share, and add the service account's email address (the
client_emailfield 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.