# Hightouch Webhooks

Hightouch is a reverse ETL platform. Its **HTTP Request destination** calls any URL you choose
whenever a sync finds rows **added**, **changed** or **removed** in your model. (The older
Webhook destination is deprecated.) Requests are **not signed**: there is no HMAC and no
timestamp. You authenticate them with a secret header that you add to the destination's headers
(tick **Secret** to encrypt it) and compare on your endpoint.

## When to Use This Skill

* How do I receive Hightouch webhooks / HTTP Request destination calls?
* How do I verify a Hightouch webhook signature? (There is none: use a secret header.)
* How do I handle Hightouch rows added, rows changed and rows removed requests?
* Why does my Hightouch sync get 401 from my endpoint?
* What does a batched Hightouch request body look like?

## Verification (core)

```javascript
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 verifyHightouchRequest(
  headers,                                   // Node lowercases header names
  secret = process.env.HIGHTOUCH_WEBHOOK_SECRET,
  headerName = process.env.HIGHTOUCH_WEBHOOK_HEADER || 'Authorization'
) {
  if (!secret) return false;                 // never accept when unconfigured
  const received = headers[headerName.toLowerCase()];
  return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}

```

* The header name is **your choice** (read from `HIGHTOUCH_WEBHOOK_HEADER`, default
  `Authorization`). The expected value is `HIGHTOUCH_WEBHOOK_SECRET`: the **exact, whole header
  value**, including any `Bearer ` prefix you typed into the destination. Look the header up
  case-insensitively and compare the whole value; don't strip or parse a prefix.
* Return `401` for a missing header or a mismatch, `400` for an unparseable body, `200` once handled.
* 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.
* Python: `hmac.compare_digest(a.encode(), b.encode())` on bytes. Never pass `str`, which raises
  on non-ASCII input.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse JSON
  only after the credential check. The body may be empty (e.g. `GET`).
* OAuth mode sends rotating access tokens and isn't covered; use a secret header instead.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/hightouch-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/hightouch-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/hightouch-webhooks/examples/fastapi/).

## Triggers

Hightouch defines no event names and writes nothing about the trigger into the request. Each
trigger has its own method, URL and Liquid payload, so the examples branch on the method. This is
a convention; match it to how you configure the sync.

| Trigger (sync config) | Typical method | Example handler name |
| --- | --- | --- |
| Rows added | `POST` | `row_added` |
| Rows changed | `PUT` or `PATCH` | `row_changed` |
| Rows removed | `DELETE` | `row_removed` |

The HTTP Request destination supports `GET`, `POST`, `PUT`, `PATCH` and `DELETE`. The example
routes accept all five. The payload is whatever Liquid template you write, so the examples use an
illustrative body (`{"id": "...", "email": "..."}`). With **batching** on (default size 100) the
template loops over `rows`, so the body is typically an array or an object wrapping one.

## Environment Variables

```bash
HIGHTOUCH_WEBHOOK_HEADER=Authorization        # header name you added to the destination
HIGHTOUCH_WEBHOOK_SECRET=your-shared-secret   # exact, whole header value (e.g. "Bearer abc123")

```

## Local Development

Hightouch can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

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

```

Use the printed URL as the base URL (and endpoint path) in the HTTP Request destination.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/hightouch-webhooks/references/overview.md) - How the HTTP Request destination works, triggers, batching, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/hightouch-webhooks/references/setup.md) - Create the destination, add the secret header, configure each trigger
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/hightouch-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/hightouch-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests