Webhooks & Polling
SheetsAPI is a pull-based REST API. This page covers polling strategies, change detection, and how to build event-driven patterns on top of it.
SheetsAPI is a pull-based REST API - it does not push events when a spreadsheet changes. This page shows how to build near-real-time and event-driven patterns using polling, change detection, and background jobs.
Why no webhooks?
Google Sheets does not expose a reliable push API for cell-level changes. SheetsAPI reads from Google Sheets on demand, so the freshest data is always one GET request away. Webhooks would require SheetsAPI to continuously watch your sheet and route events - an approach that introduces latency, reliability concerns, and significant complexity.
The polling patterns below achieve the same end result with simpler infrastructure.
Basic polling (browser)
Check for new data every N seconds:
function pollSheet(userKey: string, sheet: string, intervalMs = 30_000) {
let lastTotal = 0;
async function check() {
const resp = await fetch(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${userKey}/${sheet}?limit=1`,
);
const { meta } = await resp.json();
if (meta.total !== lastTotal) {
lastTotal = meta.total;
onNewData(); // your callback
}
}
check();
return setInterval(check, intervalMs);
}
function onNewData() {
console.log("Sheet changed - refreshing...");
// trigger full fetch
}Fetching only limit=1 keeps the change-detection request cheap - you only need meta.total.
Change detection by comparing rows
For finer-grained detection, compare a checksum of the first page:
function rowsChecksum(rows: Record<string, string>[]): string {
return rows.map((r) => Object.values(r).join("|")).join("\n");
}
let lastChecksum = "";
async function checkForChanges(userKey: string, sheet: string) {
const resp = await fetch(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${userKey}/${sheet}?limit=50&sort=-created_at`,
);
const { data } = await resp.json();
const checksum = rowsChecksum(data);
if (checksum !== lastChecksum) {
lastChecksum = checksum;
return true; // changed
}
return false;
}Server-side polling with a cron job
For server-driven automation, poll SheetsAPI on a schedule and process new rows:
Node.js (node-cron)
import cron from "node-cron";
let lastProcessedTotal = 0;
cron.schedule("*/5 * * * *", async () => {
// every 5 minutes
const resp = await fetch(`${BASE}/${USER_KEY}/Orders?limit=1`);
const { meta } = await resp.json();
if (meta.total > lastProcessedTotal) {
const newCount = meta.total - lastProcessedTotal;
const newRows = await fetch(
`${BASE}/${USER_KEY}/Orders?limit=${newCount}&offset=${lastProcessedTotal}&sort=created_at`,
).then((r) => r.json());
await processNewOrders(newRows.data);
lastProcessedTotal = meta.total;
}
});Apps Script time-based trigger
From inside Google Apps Script, call SheetsAPI on a schedule without any external cron:
function checkForNewOrders() {
const url = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Orders?limit=1";
const resp = UrlFetchApp.fetch(url, {
headers: { Authorization: "Bearer sk_your_api_key_here" },
});
const { meta } = JSON.parse(resp.getContentText());
const props = PropertiesService.getScriptProperties();
const lastTotal = parseInt(props.getProperty("lastTotal") || "0", 10);
if (meta.total > lastTotal) {
// Fetch and process new rows
props.setProperty("lastTotal", String(meta.total));
sendNotification(`${meta.total - lastTotal} new orders`);
}
}
// Run every 5 minutes via Apps Script triggerOptimistic UI with background refresh
In browser apps, show stale data immediately while refreshing in the background:
// Show cached data right away
const cached = sessionStorage.getItem("products");
if (cached) renderProducts(JSON.parse(cached));
// Refresh in background
fetch(`${BASE}/${USER_KEY}/Products?limit=50`)
.then((r) => r.json())
.then(({ data }) => {
sessionStorage.setItem("products", JSON.stringify(data));
renderProducts(data);
});React with SWR auto-refresh
SWR's refreshInterval option polls automatically:
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function useOrders() {
return useSWR(`${BASE}/${USER_KEY}/Orders?sort=-created_at&limit=20`, fetcher, {
refreshInterval: 30_000, // poll every 30s
revalidateOnFocus: true, // revalidate when tab gains focus
dedupingInterval: 10_000, // deduplicate within 10s
});
}Polling frequency guidance
| Use case | Suggested interval |
|---|---|
| Live dashboard | 10–30 seconds |
| Order/form tracking | 1–5 minutes |
| Daily report | 1 hour or cron at specific time |
| Audit log | 5–15 minutes |
Polling more frequently than every 10 seconds is rarely necessary and will quickly exhaust
your rate limit. Use limit=1 for change detection, then
fetch the full page only when a change is detected.