How to read Google Sheets data without OAuth
A practical guide to reading - and writing - Google Sheets data from any app without setting up OAuth, a Google Cloud project, or service account credentials.
Most developers who want to read data from a Google Sheet do not need OAuth. They need a URL they can call with fetch. OAuth is the right tool for a specific situation - and that situation is not "I want to show a product list from a spreadsheet."
This guide covers every realistic option for accessing Google Sheets data without an OAuth flow for callers, what each one actually costs you, and when to use which.
Why OAuth is more than most use cases need
The full Google OAuth flow for Sheets requires you to:
- Create a Google Cloud project
- Enable the Sheets API in that project
- Configure an OAuth consent screen (app name, scopes, authorized domains)
- Create an OAuth 2.0 client ID
- Implement a redirect-based authorization flow
- Store and refresh access tokens
- Handle token expiry
That is 20+ minutes of Google Cloud Console work before you write a single line of application code. And it continues to cost you: tokens expire, refresh logic breaks in production, and every person who runs your app needs to authorize it individually.
For a multi-tenant application where each user accesses their own sheets, this complexity is warranted. For an internal dashboard, a content site, a product catalog, or an internal tool where you own the data - it is not.
Option A: The CSV export URL
Google Sheets can export any tab as a CSV. The URL pattern is:
https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/export?format=csv&gid=SHEET_GID
The gid is the numeric tab ID visible in the sheet's URL.
The catch: this only works for sheets that are published, not merely shared. "Anyone with the link can view" is not the same as publishing. You must go to File → Share → Publish to web and explicitly publish the tab. Most sheets are not published this way, and enabling it can feel uncomfortable for anything that is not genuinely public data.
Even for published sheets, the CSV export has real limitations:
- Read-only. You cannot POST rows back.
- CORS blocked in browsers. Google does not set permissive CORS headers on export URLs. A plain browser
fetchwill be blocked. - No filtering, sorting, or pagination. You get the entire sheet every time and process it client-side.
- URL instability. Google occasionally changes export URL formats without notice.
It works for a quick server-side script that polls a fully public sheet. It breaks for almost everything else.
Option B: The Google Visualization API query endpoint
There is a lesser-known endpoint built into Google Sheets for its charting engine:
https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/gviz/tq?tqx=out:json&sheet=Sheet1
It accepts a limited SQL-like query language (tq parameter) and returns data in a non-standard JSON-with-padding envelope that requires stripping before parsing.
It is read-only, the response format is fragile, the query language is underdocumented, and it depends on the same publication requirements as the CSV export for browser-accessible use. It is mainly useful for Google Charts. Do not build an integration on it.
Option C: A service account (still no user OAuth, but still complex)
A service account is a robot Google identity you create in Google Cloud Console. It has no user login flow - you download a JSON key file and use it to authenticate server-to-server. This sidesteps the per-user OAuth dance.
Setup:
npm install googleapisimport { google } from "googleapis";
import credentials from "./service-account-key.json" assert { type: "json" };
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const sheets = google.sheets({ version: "v4", auth });
const response = await sheets.spreadsheets.values.get({
spreadsheetId: "YOUR_SPREADSHEET_ID",
range: "Products!A:D",
});For this to work, you must share the spreadsheet with the service account's email address (looks like name@project.iam.gserviceaccount.com).
The problems:
- You still need a Google Cloud project and you still need to enable the Sheets API.
- The JSON key file is a credential. It must not be committed to source control, must be rotated, and must be kept out of client-side bundles - it cannot safely run in a browser.
- It is Node/Deno/server-only. If you are writing a Cloudflare Worker, a browser app, or anything that runs at the edge, the
googleapispackage does not work cleanly. - Each spreadsheet needs to be explicitly shared with the service account email, which is easy to forget.
Service accounts are a solid solution for server-side integrations where you already have a Google Cloud setup. They are overkill for read-mostly cases and unavailable for browser-side code.
Option D: SheetsAPI - one auth, no credentials for callers
SheetsAPI takes a different approach. You authenticate with your Google account once through the GKit dashboard. SheetsAPI stores and manages the token internally. Your spreadsheets become plain REST endpoints - callers hit them with fetch, no OAuth, no credentials, no SDK.
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/SheetName
That URL works from a browser, a Cloudflare Worker, a mobile app, a Python script, or a curl command. No credential file, no SDK, no token refresh code.
If you want access control, you generate an API key in the dashboard and callers pass it as a Bearer token. If the sheet is public-to-callers, no token is needed at all.
SheetsAPI runs on Cloudflare Workers with CORS enabled, so browser-side fetches work without a proxy. It supports JSON, CSV, TSV, XML, and JSONP output. It is MIT-licensed and self-hostable. Free during beta with no credit card required.
Reading data - the basic case
const res = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products",
);
const { data, meta } = await res.json();
// data: [{ name: "Headphones", price: "79", category: "Audio" }, ...]
// meta: { total: 142, page: 1, limit: 50 }The response wraps rows as objects keyed by your header row. No parsing, no column index mapping.
Filtering, sorting, and paginating
Every endpoint accepts query parameters directly - no changes to your sheet, no backend code:
const url = new URL("https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products");
// Rows where category is "Audio", sorted by price, page 2
url.searchParams.set("search", "category:Audio");
url.searchParams.set("sort", "price");
url.searchParams.set("order", "asc");
url.searchParams.set("page", "2");
url.searchParams.set("limit", "25");
// Return only specific columns
url.searchParams.set("fields", "name,price,sku");
const res = await fetch(url);
const { data, meta } = await res.json();Supported params: search, sort, order, page, limit, fields, format. See the docs for the full reference.
Writing data - also no OAuth for callers
POST to the same endpoint to append a row:
const res = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Signups",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_API_KEY", // required for writes
},
body: JSON.stringify({
email: "user@example.com",
source: "landing-page",
joined: new Date().toISOString(),
}),
},
);
const { row } = await res.json();
// row: { email: "user@example.com", source: "landing-page", joined: "2026-06-30T..." }PUT updates an existing row by its row index, DELETE removes one. Full CRUD from any environment that can make HTTP requests - no SDK, no service account, no Google Cloud project on the caller's side.
When you should use the raw Sheets API with OAuth
There are real cases where the full OAuth setup is the right answer:
Multi-tenant apps. If each of your users accesses their own Google Sheet - their own data, their own Drive - each user needs to authorize your app. That is exactly what OAuth is for. SheetsAPI is not designed for this case.
Complex cell operations. If you need to read or write cell formatting, merge cells, work with named ranges, or manipulate formulas - not just row data - you need the Sheets API v4 directly. SheetsAPI exposes rows, not cell-level metadata.
Apps Script integrations. If you are building on top of Google Workspace with triggers, custom menus, or add-ons, Apps Script and the Sheets API are the right layer.
High-volume batch operations. Very large reads or writes in tight loops are better handled server-side with the official client library where you have full control over batching and quota management.
For everything else - internal dashboards, content sites, product catalogs, form submissions, team directories, event listings, lightweight backends - the overhead of full OAuth setup is real friction with no payoff. See use cases for more examples of what fits well.
Comparison
| Approach | Writes | Browser-safe | Setup time | Credentials in code |
|---|---|---|---|---|
| CSV export URL | No | No (CORS) | 2 min | None |
| GViz query endpoint | No | Requires publish | 5 min | None |
| Service account | Yes | No | 20+ min | JSON key file |
| SheetsAPI | Yes | Yes | 2 min | None for callers |
| Raw Sheets API + OAuth | Yes | Yes (with flow) | 30+ min | OAuth client |
The three-line version
Connect your sheet in the GKit dashboard, then:
const res = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Sheet1",
);
const { data } = await res.json();
console.log(data); // [{ col1: "...", col2: "...", ... }, ...]That is the whole integration. No Google Cloud project, no credentials file, no token refresh logic. Get started free - SheetsAPI is in beta, MIT-licensed, and there is no credit card required.
For the full parameter reference and output format options, see the SheetsAPI docs. To see what other developers are building with it, check the use cases page.