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

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

## Payloads

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

| Payload | Format | Identified by |
| --- | --- | --- |
| Forwarded event | Amplitude [event format](https://amplitude.com/docs/apis/analytics/export) | `event_type` is **your own event name** (for example `Button Clicked`), plus `user_id`, `event_time`, `event_properties`, `user_properties`, ... |
| User properties update | Amplitude [user format](https://amplitude.com/docs/apis/analytics/identify) | `user_id` and/or `device_id` plus `user_properties`, sent when Amplitude receives an event or an Identify API call |

An [Apache FreeMarker](https://freemarker.apache.org/) 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

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

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

```

Use the printed URL as the webhook URL in Amplitude.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/amplitude-webhooks/references/overview.md) - What the Webhook destination sends, payload shapes, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/amplitude-webhooks/references/setup.md) - Create the sync, add the secret header, enable forwarding
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/amplitude-webhooks/references/verification.md) - Secret header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/amplitude-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests