# 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.

```javascript
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/estuary-webhooks/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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/estuary-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/estuary-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/estuary-webhooks/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

```bash
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

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

```

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

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/estuary-webhooks/references/overview.md) - Triggers vs the HTTP Webhook connector, payloads, methods
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/estuary-webhooks/references/setup.md) - Adding the secret header and configuring the URL and method
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/estuary-webhooks/references/verification.md) - Header check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/estuary-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests