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, defaultAuthorization). The expected value isHIGHTOUCH_WEBHOOK_SECRET: the exact, whole header value, including anyBearerprefix you typed into the destination. Look the header up case-insensitively and compare the whole value; don't strip or parse a prefix. - Return
401for a missing header or a mismatch,400for an unparseable body,200once 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_on bytes. Never passdigest(a.encode(), b.encode()) 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 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
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
- references/overview.md - How the HTTP Request destination works, triggers, batching, payloads
- references/setup.md - Create the destination, add the secret header, configure each trigger
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests