Webhooks & Change Notifications
Configure GKit to push HTTP callbacks when your spreadsheet data changes - polling alternatives, payload shape, retry logic, and security verification.
Overview
GKit can push HTTP notifications to your server whenever spreadsheet data changes. Instead of polling the Sheets API on a schedule, you register a webhook endpoint once and receive a request the moment a relevant event occurs. This is the recommended approach for real-time integrations, audit pipelines, and data-sync workflows.
For simple use cases where latency is not critical, polling remains a valid option.
Supported Events
GKit fires webhook notifications for the following event types:
| Event type | Description |
|---|---|
row.inserted | One or more rows were appended or inserted into a sheet |
row.updated | Existing row values were modified |
row.deleted | One or more rows were removed |
bulk.import | A batch write or import operation affected many rows at once |
Each notification carries exactly one event type. Bulk imports are delivered as a single payload rather than individual row.inserted events, so your handler should be prepared for either shape.
Registering a Webhook
Send a POST request to /v1/webhooks with the target URL and the sheet you want to monitor. You can optionally scope notifications to specific event types; omitting events subscribes you to all of them.
POST https://api.gkit.io/v1/webhooks
Authorization: Bearer <your-api-key>
Content-Type: application/json
{
"url": "https://your-app.example.com/hooks/gkit",
"spreadsheet_id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
"sheet_name": "Orders",
"events": ["row.inserted", "row.updated", "row.deleted"]
}A successful response returns 201 Created with a webhook_id and a secret. Store the secret in an environment variable - it is shown only once and is required for signature verification.
Payload Shape
Every delivery is a POST request with a JSON body and the Content-Type: application/json header. The payload follows this structure:
{
"webhook_id": "wh_01j2k3m4n5p6q7r8",
"event": "row.updated",
"spreadsheet_id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
"sheet_name": "Orders",
"timestamp": "2026-06-29T14:32:00.000Z",
"changed_rows": [
{
"row_index": 42,
"before": { "Status": "Pending", "Amount": 150 },
"after": { "Status": "Shipped", "Amount": 150 }
}
]
}changed_rowscontains the row index (1-based), the cell values before the change, and the values after. Forrow.insertedevents thebeforekey isnull; forrow.deletedevents theafterkey isnull.- For
bulk.importevents,changed_rowsmay contain up to 500 entries. If the import exceeded that limit, a top-level"truncated": truefield is included in the payload.
Signature Verification
Every request includes an X-GKit-Signature header containing an HMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret. Verify this header before processing any payload.
// Express / Node.js example
const crypto = require("crypto");
const WEBHOOK_SECRET = process.env.GKIT_WEBHOOK_SECRET;
app.post("/hooks/gkit", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-gkit-signature"];
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body) // raw Buffer - do not parse before hashing
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// process event...
res.sendStatus(200);
});Use express.raw() rather than express.json() so the body buffer is available before parsing. The comparison uses timingSafeEqual to prevent timing attacks.
Retry Policy
GKit considers a delivery successful when your endpoint returns an HTTP 2xx status within 5 seconds. If the request times out or returns any other status, GKit retries up to 3 times using exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
After all retries are exhausted the delivery is marked as failed. You can view failed deliveries and trigger a manual replay from the GKit dashboard under Webhooks > Delivery Log.
Design your handler to be idempotent - network conditions can occasionally cause a successful delivery to be retried. Use the webhook_id combined with the timestamp as a deduplication key if needed.
Polling as an Alternative
If you prefer not to expose a public HTTPS endpoint, you can poll the Sheets API for changes using the updated_since query parameter:
GET /v1/spreadsheets/{id}/rows?sheet=Orders&updated_since=2026-06-29T14:00:00Z
Authorization: Bearer <your-api-key>Polling is simpler to set up but introduces latency equal to your poll interval and consumes more API quota. Webhooks are the preferred approach for production workloads where responsiveness matters.