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
Authorizationheader 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
addandupdateevents?
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 type | Fires when |
|---|---|
add | A row is added to the table (or its Ready column becomes true) |
update | An 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
- references/overview.md - Row payloads, event types, retries and queue
- references/setup.md - Creating the webhook and Header Authorization in Grist
- references/verification.md - Header check in Node, Python and Next.js, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests