Agent skill
Hasura Webhooks Skill
Receive and authenticate Hasura Event Trigger webhooks. Use when setting up a Hasura v2 Event Trigger endpoint, checking the secret header you add to the trigger, debugging 401s, or handling INSERT, UPDATE, DELETE and MANUAL events.
Install this skill
npx skills add hookdeck/webhook-skills --skill hasura-webhooks
Hasura v2 Event Triggers call your webhook with a JSON payload whenever a row is inserted, updated or deleted in a Postgres or MS SQL Server table (or when you invoke the trigger manually). This skill covers Hasura v2 Event Triggers only, not Hasura DDN (v3).
Event Trigger requests are not signed. To authenticate them you add a secret header to the trigger (Advanced settings > Headers) and your endpoint compares that header with the value you stored. The header name is yours to choose.
When to Use This Skill
- How do I receive Hasura Event Trigger webhooks?
- How do I authenticate requests from a Hasura Event Trigger?
- Does Hasura sign webhook payloads? (No: use a secret header)
- How do I handle
INSERT,UPDATE,DELETEandMANUALevents? - Why does my Hasura webhook endpoint return 401?
Verification (core)
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 verifyHasuraRequest(headers, {
name = process.env.HASURA_WEBHOOK_HEADER || 'Authorization',
secret = process.env.HASURA_WEBHOOK_SECRET,
} = {}) {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // Node lowercases header names
return Boolean(received) && safeEqual(received, secret); // compare the WHOLE value
}
- The header is whatever you configured on the trigger.
HASURA_WEBHOOK_HEADERholds its name (defaultAuthorization);HASURA_WEBHOOK_SECRETholds the exact, whole header value, including anyBearerprefix you typed. Don't strip or parse a prefix. - Look the header up case-insensitively. Return
401when it is missing or doesn't match. - Read the body raw (
express.raw({ type: '*/*' })) and parse JSON only after the check.
A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so a captured request can be replayed and the body isn't integrity-protected. Use HTTPS, rotate the credential if it leaks, and make handlers idempotent (use the event
id).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The operation is in event.op. Dispatch on it:
event.op | Sent When | event.data.old / event.data.new |
|---|---|---|
INSERT | A row is inserted | old is null, new is the row |
UPDATE | A row is updated (only if the new data differs) | before / after values |
DELETE | A row is deleted | old is the row, new is null |
MANUAL | The trigger is invoked manually (console or API) | old is null, new is the current row |
Payload shape: { event: { op, data: { old, new }, session_.
Environment Variables
HASURA_WEBHOOK_HEADER=Authorization # name of the header you added to the trigger
HASURA_WEBHOOK_SECRET=your-header-value # exact whole value of that header
Local Development
Hasura can't reach localhost from Hasura Cloud. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 hasura --path /webhooks/hasura
Use the printed URL as the trigger's Webhook URL.
Resources
- references/overview.md - What Event Triggers are, ops and payload shape
- references/setup.md - Create the trigger and add the secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests