How to send data to Google Sheets via REST API
A practical guide to writing data to Google Sheets from any app, script, or service using a REST API - no Google SDK, no Apps Script, no OAuth setup for callers.
Most guides on the Google Sheets API cover reading data. This one covers writing it - POST, PUT, and DELETE - from any language or runtime, without touching the Google SDK or setting up OAuth.
Why the official API is harder than it looks
The Google Sheets API v4 is powerful, but it was built for Google Workspace integrations, not general-purpose REST consumers. To write data, you need all of this before you send a single request:
- A Google Cloud project with the Sheets API enabled
- A service account (or OAuth 2.0 client) with credentials JSON downloaded locally
- The
spreadsheetsscope -spreadsheets.readonlyis not enough for writes - An understanding of whether to use
values.append,values.update, orbatchUpdate- each has a different payload shape and different behavior around existing data
That is a reasonable setup for a Google Workspace app. It is a lot for a form submission handler, a log aggregator, or a weekend project.
SheetsAPI is a REST adapter in front of your Google Sheets. You connect a sheet once through the GKit dashboard, and from that point any app can write rows using plain HTTP. The caller never touches OAuth. There is no SDK to install. POST a JSON object; it becomes a row.
The endpoint pattern
After connecting a sheet at gkit.mreshank.com/dashboard, you get a base URL:
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/{userKey}/{sheetName}
{userKey}- the key shown in your dashboard after connecting{sheetName}- the tab name exactly as it appears in the spreadsheet
The first row of each tab is the header row. Those cell values become the field names. A sheet with columns name, email, source, created_at accepts objects with exactly those keys.
All write operations require an API key. Create one in the dashboard under API Keys and pass it as a Bearer token on every request:
Authorization: Bearer sk_live_...
POST - append a row
A POST to the collection endpoint appends one object as a new row at the bottom of the sheet. The body is a single JSON object whose keys match your header columns.
JavaScript (fetch)
const BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
const KEY = process.env.GKIT_API_KEY;
const res = await fetch(`${BASE}/uk_abc123/Leads`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${KEY}`,
},
body: JSON.stringify({
name: "Jordan Blake",
email: "jordan@example.com",
source: "landing-page",
created_at: new Date().toISOString(),
}),
});
const { data } = await res.json();
// data: { row: 42, name: "Jordan Blake", email: "jordan@example.com", ... }Python
import os
import requests
from datetime import datetime, timezone
BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets"
KEY = os.environ["GKIT_API_KEY"]
resp = requests.post(
f"{BASE}/uk_abc123/Leads",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {KEY}",
},
json={
"name": "Jordan Blake",
"email": "jordan@example.com",
"source": "landing-page",
"created_at": datetime.now(timezone.utc).isoformat(),
},
)
resp.raise_for_status()
print(resp.json()["data"]["row"]) # 42curl
curl -X POST \
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/uk_abc123/Leads \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_..." \
-d '{
"name": "Jordan Blake",
"email": "jordan@example.com",
"source": "landing-page",
"created_at": "2026-07-01T00:00:00Z"
}'PHP
<?php
$url = 'https://sheetsapi.gkit.mreshank.com/api/spreadsheets/uk_abc123/Leads';
$key = $_ENV['GKIT_API_KEY'];
$body = json_encode([
'name' => 'Jordan Blake',
'email' => 'jordan@example.com',
'source' => 'landing-page',
'created_at' => gmdate('c'),
]);
$opts = [
'http' => [
'method' => 'POST',
'header' => implode("\r\n", [
'Content-Type: application/json',
"Authorization: Bearer {$key}",
]),
'content' => $body,
],
];
$response = file_get_contents($url, false, stream_context_create($opts));
$data = json_decode($response, true);
echo $data['data']['row']; // 42Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
func main() {
base := "https://sheetsapi.gkit.mreshank.com/api/spreadsheets"
key := os.Getenv("GKIT_API_KEY")
payload := map[string]string{
"name": "Jordan Blake",
"email": "jordan@example.com",
"source": "landing-page",
"created_at": time.Now().UTC().Format(time.RFC3339),
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest(http.MethodPost, base+"/uk_abc123/Leads", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result["data"].(map[string]any)["row"]) // 42
}Ruby
require "net/http"
require "json"
require "time"
base = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets"
key = ENV.fetch("GKIT_API_KEY")
uri = URI("#{base}/uk_abc123/Leads")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{key}"
req.body = JSON.generate(
name: "Jordan Blake",
email: "jordan@example.com",
source: "landing-page",
created_at: Time.now.utc.iso8601
)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
puts data.dig("data", "row") # 42Bulk insert - POST an array
To append multiple rows in one request, pass an array of objects instead of a single object. The API inserts them in order and returns the row numbers assigned to each.
const rows = [
{ name: "Ada Lovelace", email: "ada@example.com", source: "import" },
{ name: "Grace Hopper", email: "grace@example.com", source: "import" },
{
name: "Margaret Hamilton",
email: "mhamilton@example.com",
source: "import",
},
];
const res = await fetch(`${BASE}/uk_abc123/Leads`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${KEY}`,
},
body: JSON.stringify(rows),
});
const { data } = await res.json();
// data: [{ row: 43, name: "Ada Lovelace", ... }, { row: 44, ... }, { row: 45, ... }]curl -X POST \
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/uk_abc123/Leads \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_..." \
-d '[
{ "name": "Ada Lovelace", "email": "ada@example.com", "source": "import" },
{ "name": "Grace Hopper", "email": "grace@example.com", "source": "import" },
{ "name": "Margaret Hamilton", "email": "mhamilton@example.com", "source": "import" }
]'Bulk inserts count as a single write against the quota. For large imports (thousands of rows), batch in chunks of 500 - see the rate limits section below.
PUT - update a specific row
A PUT targets a specific row by its row number. The row number is returned by POST and is also visible in the sheet itself - the first data row (below headers) is row 2.
JavaScript
const rowNumber = 42;
const res = await fetch(`${BASE}/uk_abc123/Leads/${rowNumber}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${KEY}`,
},
body: JSON.stringify({
status: "qualified",
updated_at: new Date().toISOString(),
}),
});
const { data } = await res.json();
// data: { row: 42, name: "Jordan Blake", status: "qualified", ... }You only need to send the fields you want to change. Fields not included in the body are left as-is.
curl
curl -X PUT \
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/uk_abc123/Leads/42 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_..." \
-d '{ "status": "qualified", "updated_at": "2026-07-01T06:00:00Z" }'For a complete walkthrough of all four operations, see the CRUD reference.
DELETE - remove a row
A DELETE to the row URL removes that row and shifts all rows below it up by one. The response returns the deleted row's data.
JavaScript
const rowNumber = 42;
const res = await fetch(`${BASE}/uk_abc123/Leads/${rowNumber}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${KEY}`,
},
});
const { data } = await res.json();
// data: { row: 42, name: "Jordan Blake", email: "jordan@example.com", ... }curl
curl -X DELETE \
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/uk_abc123/Leads/42 \
-H "Authorization: Bearer sk_live_..."Because DELETE shifts row numbers, avoid deleting multiple rows in parallel. Delete them sequentially from the bottom up, or re-fetch row numbers between deletions.
Real use cases for sending data to Sheets
Form submissions. Replace a backend form handler with a POST. Your contact, waitlist, or survey form sends data directly to a sheet. No database to provision, no admin panel to build - the sheet is the admin panel.
Webhook receivers. Stripe sends a payment_intent.succeeded event. Your handler normalizes the payload and POSTs the relevant fields to a Payments sheet. The finance team gets a live feed in a tool they already use. The webhook pattern guide covers this in detail.
IoT and sensor data. A microcontroller or edge device reads a sensor and POSTs { device_id, temperature, humidity, timestamp } over HTTPS. The sheet becomes the time-series log - no InfluxDB, no Grafana setup for a prototype.
Log aggregation. Server-side events, deploys, errors, or audit actions. POST { event, user_id, metadata, timestamp }. The sheet stores the log and makes it searchable and shareable without additional tooling.
CRM activity capture. A sales tool or internal script POSTs every call, email, or meeting note to a CRM_Activity sheet. The rep sees their history in a sheet they can filter and sort however they want.
E-commerce order logging. Every completed checkout triggers a POST to an Orders sheet. Operations gets real-time visibility into order volume without waiting for a dashboard build.
All of these follow the same pattern: normalize the data in your handler, POST it to the sheet endpoint, move on. See the use cases page for more.
Authentication for writes
GET requests on public sheets work without a key. Every write operation - POST, PUT, DELETE - requires one.
Create an API key in the GKit dashboard under API Keys. Keys are scoped: you can restrict a key to read-only, or to a specific sheet, so a form handler cannot touch your other sheets.
Send the key as a Bearer token on every write request:
Authorization: Bearer sk_live_abc123...
If a request arrives without a valid key, the API returns 401 Unauthorized. The sheet is never modified.
Rate limits for writes
Google's Sheets API imposes a hard limit of 60 write operations per minute per spreadsheet. SheetsAPI queues concurrent writes on your behalf, so you will not blow past this limit with a burst of requests - but sustained throughput above 60 writes/minute will back up.
Practical guidance:
- For bulk inserts, use the array POST (one API call for many rows) rather than looping individual POSTs.
- For large imports, batch at 500 rows per request and space requests 5–10 seconds apart.
- Do not fire 100 parallel POSTs. The queue handles the backpressure, but your caller will sit waiting for responses.
- For high-frequency telemetry (IoT, logging), buffer writes client-side and flush in batches.
See the rate limits guide for the full breakdown.
SheetsAPI vs Google Sheets API v4 vs Apps Script doPost
This question comes up often enough that it is worth a direct comparison.
| SheetsAPI | Sheets API v4 | Apps Script doPost | |
|---|---|---|---|
| Setup time | ~2 minutes | 30–60 minutes | 15–30 minutes |
| Auth for callers | API key (Bearer token) | OAuth 2.0 or service account | None (URL is public) or HMAC |
| CORS support | Yes | No (server-side only) | Limited, manual |
| Write format | Plain JSON object | values.append range/body | Custom, parsed from e.postData |
| Partial update | Yes (send only changed fields) | No (must specify range explicitly) | Manual |
| Bulk insert | Yes (array of objects) | Yes (values array) | Manual loop |
| Rate limiting | Queued automatically | Your responsibility | Your responsibility |
| Maintenance | None | SDK + credential rotation | Apps Script project |
Apps Script doPost works well for simple automations inside Google Workspace. The Sheets API v4 is the right choice when you are building a Google Workspace integration and already have the OAuth infrastructure. SheetsAPI fits everything else - external apps, scripts, third-party services, and any context where you want to send data to a sheet without managing Google credentials.
Get started
Connect a sheet at gkit.mreshank.com/dashboard. The endpoint is live in under two minutes.
The SheetsAPI product page has the full parameter reference, filter syntax, and pagination docs. The pricing page covers the free tier - it is generous for most use cases. If you are curious about what else GKit does, the about page and free tools are worth a look.
For the complete four-method CRUD reference, see Full CRUD on Google Sheets via REST API.