Agent skill

Amplitude Webhooks Skill

Receive and authenticate Amplitude webhooks (the Webhook streaming destination). Use when setting up an Amplitude webhook endpoint, checking the secret header you add to the destination, debugging 401s, or handling forwarded events and user property updates.

Install this skill

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


Amplitude's Webhook destination (Events and User Properties, on the Plus, Growth and Enterprise plans) streams events and user updates to a URL you choose. Requests are not signed. Every request carries two preset headers, Content-Type: application/json and User-Agent: Amplitude/Webhook/1.0, plus up to five custom headers you define. Add a secret as one of those headers and have your endpoint compare it with the value you stored.

When to Use This Skill

  • How do I receive Amplitude webhooks?
  • Does Amplitude sign webhook payloads? (No: use a secret header)
  • How do I authenticate requests from an Amplitude Webhook destination?
  • How do I handle forwarded Amplitude events and user property updates?
  • Why does my Amplitude webhook endpoint return 401?

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 verifyAmplitudeRequest(
  headers,
  secret = process.env.AMPLITUDE_WEBHOOK_SECRET,
  headerName = process.env.AMPLITUDE_WEBHOOK_HEADER || 'Authorization'
) {
  if (!secret) return false;                        // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
  • The header name is whatever you typed in the destination's header rows, so read it from AMPLITUDE_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • AMPLITUDE_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Compare the whole value; do not strip or parse a prefix.
  • Return 401 for a missing or mismatched header, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.
  • In Python, use hmac.compare_digest on bytes (.encode() both sides); passing str raises on non-ASCII input.

Security note: a static shared credential authenticates the sender, not the body, and gives no replay protection (there is no signature and no timestamp). Use HTTPS, and rotate the secret in the destination and in your environment if it leaks.

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

Payloads

Amplitude does not use a fixed list of webhook event names. The default payloads are:

PayloadFormatIdentified by
Forwarded eventAmplitude event formatevent_type is your own event name (for example Button Clicked), plus user_id, event_time, event_properties, user_properties, ...
User properties updateAmplitude user formatuser_id and/or device_id plus user_properties, sent when Amplitude receives an event or an Identify API call

An Apache FreeMarker template can emit any JSON instead, which changes these shapes. The examples handle the default format; adapt the dispatch if you use a template.

Environment Variables

AMPLITUDE_WEBHOOK_HEADER=Authorization       # name of the header you added in Amplitude
AMPLITUDE_WEBHOOK_SECRET=your-secret-value   # its exact, whole value (including any "Bearer ")

Local Development

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

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

Use the printed URL as the webhook URL in Amplitude.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →