Agent skill

Estuary Webhooks Skill

Receive and authenticate Estuary webhooks. Use when setting up an endpoint for Estuary materialization triggers or the HTTP Webhook materialization connector, checking the secret header you configure (Estuary does not sign requests), debugging 401s, or handling POST, PUT or PATCH deliveries.

Install this skill

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


Estuary (real-time data integration) calls your endpoint from materialization triggers (one request per committed transaction) and from the HTTP Webhook materialization connector (which sends documents). Requests are not signed: there is no signature header, no HMAC and no timestamp. You authenticate by adding a secret header yourself.

Hookdeck's Estuary source setup note says the same thing:

Estuary triggers are not signed. Add a secret header to the materialization trigger (or the HTTP Webhook connector) and enter it here.

When to Use This Skill

  • How do I receive Estuary materialization trigger webhooks?
  • How do I authenticate requests from the Estuary HTTP Webhook connector?
  • Does Estuary sign webhooks? (No: a header you configure, such as Authorization: Bearer ...)
  • Why does my Estuary webhook endpoint return 401?
  • Which HTTP methods does an Estuary trigger use? (POST by default, or PUT or PATCH)

Verification (core)

A trigger's headers are "HTTP headers to include in the request" and are encrypted at rest; the HTTP Webhook connector takes /headers/customHeaders. The header name is yours to choose, so read it from ESTUARY_WEBHOOK_HEADER (default Authorization) and compare the whole value with ESTUARY_WEBHOOK_SECRET. Do not strip or parse a Bearer prefix: if you typed Bearer my-secret-token as the header value, that exact string is the secret.

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
const digest = (buf) => crypto.createHash('sha256').update(buf).digest();

function verifyEstuaryRequest(headers, env = process.env) {
  const expected = env.ESTUARY_WEBHOOK_SECRET;
  if (!expected) return false;                         // never accept when unconfigured
  const name = (env.ESTUARY_WEBHOOK_HEADER || 'Authorization').toLowerCase();
  const received = headers[name];                      // Node lowercases header names
  if (!received) return false;
  return crypto.timingSafeEqual(
    digest(Buffer.from(received, 'latin1')), digest(Buffer.from(expected, 'utf8')));
}

Return 401 on a missing header or mismatch, for every HTTP method, before reading the body. Read the body raw (express.raw({ type: '*/*' })) so any content type works and parse it only after the check. 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 if it leaks.

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

HTTP Methods

Triggers default to POST and can use PUT or PATCH ("One of: POST, PUT, PATCH"). The examples therefore accept GET, POST, PUT, PATCH and DELETE on the route, check the credential first for each, and handle an empty body.

Hookdeck's Estuary source pre-selects POST without managing the method, so if your trigger uses PUT or PATCH, also allow that method on the Hookdeck source.

Payloads

There is no vendor event field. A trigger's body is the payloadTemplate its author wrote (template variables include materialization_name, collection_names, flow_published_at_min, flow_published_at_max and run_id); the HTTP Webhook connector sends documents. The examples use a clearly labelled illustrative payload and tell a trigger body (has materialization_name) from any other document.

Non-retryable 4xx responses fail the trigger, so return 2xx once you have accepted the request and 401 only for a bad credential.

Environment Variables

ESTUARY_WEBHOOK_HEADER=Authorization            # the header name you configured in Estuary
ESTUARY_WEBHOOK_SECRET=Bearer my-secret-token   # the exact, whole header value

Local Development

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

No account required. Use the printed URL as the trigger url.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →