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, DELETE and MANUAL events?
  • 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_HEADER holds its name (default Authorization); HASURA_WEBHOOK_SECRET holds the exact, whole header value, including any Bearer prefix you typed. Don't strip or parse a prefix.
  • Look the header up case-insensitively. Return 401 when 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.opSent Whenevent.data.old / event.data.new
INSERTA row is insertedold is null, new is the row
UPDATEA row is updated (only if the new data differs)before / after values
DELETEA row is deletedold is the row, new is null
MANUALThe trigger is invoked manually (console or API)old is null, new is the current row

Payload shape: { event: { op, data: { old, new }, session_variables, trace_context }, created_at, id, delivery_info: { current_retry, max_retries }, trigger: { name }, table: { schema, name } }.

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →