Agent skill

Grist Webhooks Skill

Receive and authenticate Grist document webhooks. Use when setting up a Grist webhook endpoint (Document settings > Webhooks), checking the static Authorization header, debugging 401s, or handling the JSON array of added or updated rows Grist sends for the add and update event types.

Install this skill

npx skills add hookdeck/webhook-skills --skill grist-webhooks


Grist (spreadsheet-database) can notify an external URL whenever rows are added to, or modified in, one table of a document. Payloads are not signed. The only credential is the optional Header Authorization field, a static string Grist sends verbatim as the Authorization header.

When to Use This Skill

  • How do I receive Grist webhooks?
  • How do I check the Authorization header Grist sends with a webhook?
  • Does Grist sign webhook payloads? (No: static header credential only)
  • Why does my Grist webhook endpoint return 401?
  • How do I handle the JSON array of rows in a Grist webhook, for add and update events?

Verification (core)

In Grist, set Header Authorization to the credential, for example Bearer my-secret-token. Paste that value exactly as set in Grist, including any "Bearer " or "Basic " prefix. The key is the whole header value: compare the full Authorization header to it. Do not strip, parse or base64-decode the prefix.

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyGristRequest(headers, expected = process.env.GRIST_AUTHORIZATION) {
  if (!expected) return false;                 // never accept when unconfigured
  const received = headers['authorization'];   // full value, e.g. "Bearer my-secret-token"
  return Boolean(received) && safeEqual(received, expected);
}

Return 401 when the header is missing or does not match, 400 for a body that is not a JSON array of row objects, and 200 once handled. Read the body raw (express.raw({ type: '*/*' })) and parse it only after the check, so any content type works. Python and Next.js equivalents are in references/verification.md.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential in Grist if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Payload and Event Types

The body is a JSON array of row objects (id plus the table's column values), for example [{"id": 29, "manualSort": 29, "Title": "Trophy", "Price": 60}]. Several rows can arrive in one request. There is no event field in the payload: the event types are chosen in the webhook's config.

Event typeFires when
addA row is added to the table (or its Ready column becomes true)
updateAn existing row is modified (optionally only for the filtered columns)

Grist retries a failed delivery periodically until it succeeds, so make handlers idempotent (dedupe on the row id plus the values you care about).

Environment Variables

GRIST_AUTHORIZATION=Bearer my-secret-token   # full Header Authorization value, prefix included

Local Development

npx hookdeck-cli listen 3000 grist --path /webhooks/grist

No account required. Use the printed URL as the webhook URL in Grist.

Hookdeck Source Setup

Hookdeck has a Grist source type. When creating the source, paste the Header Authorization value exactly as set in Grist, including any "Bearer " or "Basic " prefix.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →