Agent skill

Hightouch Webhooks Skill

Receive and authenticate requests from the Hightouch HTTP Request destination (reverse ETL). Use when building an endpoint that a Hightouch sync calls, when asking how to verify a Hightouch webhook signature (there is none: add a secret header to the destination and compare its whole value in constant time), when handling rows added, changed and removed triggers with POST, PUT, PATCH or DELETE, or when debugging 401s from a Hightouch sync.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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/, examples/nextjs/, 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 methodExample handler name
Rows addedPOSTrow_added
Rows changedPUT or PATCHrow_changed
Rows removedDELETErow_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

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):

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →