Data in & out

Two directions, no backend required either way. Ingest URLs bring rows in: any service that can send a webhook can fill a sheet. Webhooks send changes out: we call your endpoint when rows change.

In: ingest URLs

Create one (needs auth), then hand the URL to Stripe, a form, CI, a sensor, anything:

terminal
sheets ingest wbk_a8Kx92mQ3Fpq add stripe-events
# → https://sheets.fast/ingest/ingtok_...

POSTed JSON becomes rows. Fields map to columns by header name, case-insensitive:

what the service sends
curl -X POST https://sheets.fast/ingest/ingtok_... \
  -H 'content-type: application/json' \
  -d '{"Date": "2026-08-30", "Customer": "Globex", "Amount": 4800}'

An empty sheet takes its header row from the first event. Arrays of objects append several rows; {"rows": [[...]]} appends raw rows. Every ingest fires the workbook's row.added webhooks, so data can flow straight through.

Out: webhooks

Subscribe an https URL to a workbook and we call it when rows change. Events: row.added, row.changed, row.deleted. Human edits in the grid and agent writes over the API fire the same events. Failed deliveries retry 3 times with backoff. Webhooks require an account: create one free, then use an API key (or a signed-in session).

Create a subscription

curl -X POST https://sheets.fast/api/v1/workbooks/wbk_a8Kx92mQ3Fpq/webhooks \
  -H 'x-api-key: sk_live_…' \
  -H 'content-type: application/json' \
  -d '{"url": "https://example.com/hooks/sheets", "events": ["row.added", "row.changed"]}'

The response includes the signing secret (whsec_ prefix) a single time. Store it. It is not retrievable later.

Payload

{
  "id": "evt_1TvfOco8n1z9",
  "test": false,
  "workbook": {"id": "wbk_a8Kx92mQ3Fpq", "name": "Q3 pipeline", "url": "https://sheets.fast/wbk_a8Kx92mQ3Fpq"},
  "revision": 7,
  "events": [
    {"type": "row.added", "sheet": "Sheet1", "row": 4, "cells": {"A4": "East", "B4": "300"}}
  ],
  "created_at": "2026-08-29T20:24:42.000Z"
}

Verify the signature

Every delivery carries X-SheetsFast-Signature: t=<unix>,v1=<hex> where v1 is HMAC-SHA256 of `${t}.${rawBody}` with your signing secret.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret: string, signature: string, rawBody: string): boolean {
  const t = signature.match(/t=(\d+)/)?.[1];
  const v1 = signature.match(/v1=([a-f0-9]+)/)?.[1];
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min tolerance
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Test and inspect

# send a sample payload (marked "test": true) through the real pipeline
curl -X POST .../workbooks/{id}/webhooks/{whk_id}/test

# recent deliveries with status
curl .../workbooks/{id}/webhooks/{whk_id}/deliveries

Delivery statuses: enqueued, delivered, failed. The workbook page has the same controls under Webhooks.