Gareth Wilson Gareth Wilson

Guide to Amplitude Webhooks: Features and Best Practices

Published


Amplitude webhooks send your product analytics data somewhere else as it arrives. When a user clicks a button, accepts your terms or updates their profile and Amplitude ingests the event, the Webhook destination can forward it to your own service, so you can trigger workflows, sync user properties into another tool, or reshape the data for a system Amplitude has no native integration with.

This guide covers what the Webhook destination sends and when, how to authenticate requests with a header you add yourself, how Amplitude retries failures, and the practices that keep an Amplitude receiver reliable in production.

What are Amplitude webhooks?

Amplitude's webhooks are a streaming destination called Webhook: Events · User Properties, available on the Plus, Growth and Enterprise plans. You create a sync, give it a URL, and choose whether it forwards events, users or both. Amplitude then sends a JSON POST to that URL each time it ingests a matching event or receives a user update.

There is no list of Amplitude webhook event types. A forwarded event is one of your own tracked events, so its event_type is whatever you called it, such as Button Clicked. A user update carries a user's properties. If neither default shape suits your receiver, you can write an Apache FreeMarker template that turns each event or user into whatever JSON you need.

Amplitude does not sign these requests. Every request has two preset headers, and you can add up to five more. Adding a secret as one of those headers, and checking it on arrival, is the only way to tell an Amplitude request from anyone else's.

Amplitude webhook features

FeatureDetails
ConfigurationAmplitude Data > Catalog > Destinations > Webhook: Events · User Properties
PlansPlus, Growth and Enterprise
What is sentEvents as Amplitude ingests them, and users when Amplitude receives an event or an Identify API call
Payload formatAmplitude's event or user format, or a FreeMarker template's output
Event namesYour own event_type values. Amplitude defines none
Method and content typePOST, Content-Type: application/json
Preset headersContent-Type: application/json and User-Agent: Amplitude/Webhook/1.0
Custom headersUp to five, name and value of your choice
AuthenticationNone built in. Add a secret custom header
Signature and timestampNone
RetriesOne attempt, then nine more over 4 hours on any failure
In-attempt retriesImmediate retries on 5xx or 429: at most 3 attempts, starting at 100 ms and doubling with 50% jitter, nothing after 4 seconds
Source IPsNot published. Amplitude says it has no single forwarding IP

The destination does not send events on a schedule or on demand, and it does not support transformed events.

Payloads

With the default format, a forwarded event follows Amplitude's event format: event_type, user_id, device_id, event_time, event_properties, user_properties and the other fields from the Export API. Times are UTC strings such as 2022-02-28 20:07:01.795.

A user update follows Amplitude's user format, with user_id and/or device_id and the user's user_properties. Which identifiers appear depends on how you call Identify: if you identify by device_id, the payload has no user_id.

A FreeMarker template replaces these shapes entirely. Amplitude's own example sends an event as:

{
  "external_id": "some user id",
  "name": "click event",
  "time": "2022-10-24T20:07:32.123",
  "properties": {
    "email": "some@email.com"
  }
}

Templates read the source event as input, so input.event_type and input.user_properties.email are available, and <#if input.user_id??> leaves a field out when the value is missing. Because the template defines the body, your receiver has to parse whatever your template emits, and a template change is a contract change.

Inspect Amplitude webhooks as they arrive. Point an Amplitude destination at a Hookdeck Inspect URL to see the exact headers and body it sends, with no account or setup required.

Setting up Amplitude webhooks

Generate a long random secret first, for example with openssl rand -hex 32, and decide which header it will travel in.

  1. In Amplitude Data, click Catalog and open the Destinations tab.
  2. Search for "Webhook", select Webhook: Events · User Properties, enter a sync name and click Create Sync.
  3. Enter your HTTPS endpoint URL.
  4. Below the two preset headers, add your secret header: a name such as X-Amplitude-Secret in the left box, and the value in the right box. Whatever you type is sent as-is, so include Bearer if you use Authorization and want a bearer token.
  5. Under Send Events, turn the toggle on, choose the default payload or a FreeMarker template, and use Select and filter events to pick only the events you need.
  6. Under Send Users, turn the toggle on if you want user updates too, and choose a format.
  7. Set Status to Enabled at the top of the page and click Save.

To put Hookdeck in front of your service, create a Hookdeck source with the Amplitude type and use its URL in step 3. The source asks for a Header Name and a Header Value: enter the same name and the whole value you typed in Amplitude. Header names match case-insensitively, but the value must match exactly, prefix included. The source accepts POST, which is the only method Amplitude uses.

For local development, hookdeck listen 3000 amplitude --path /webhooks/amplitude gives you a public URL that forwards to your machine.

Securing Amplitude webhooks

With no signature, verification means reading the header you chose and comparing its whole value with the one you stored, in constant time. Reject the request if the header is missing or the secret is not configured.

const crypto = require('crypto');
const express = require('express');

const HEADER_NAME = (process.env.AMPLITUDE_WEBHOOK_HEADER || 'x-amplitude-secret').toLowerCase();
const SECRET = process.env.AMPLITUDE_WEBHOOK_SECRET;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/amplitude', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME); // case-insensitive lookup
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  if (typeof payload.event_type === 'string') {
    // A forwarded event: payload.event_type is your own event name.
  } else if (payload.user_id !== undefined || payload.device_id !== undefined) {
    // A user properties update.
  }
  // With a FreeMarker template, branch on the fields your template emits instead.

  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

HEADER_NAME = os.environ.get("AMPLITUDE_WEBHOOK_HEADER", "X-Amplitude-Secret")
SECRET = os.environ.get("AMPLITUDE_WEBHOOK_SECRET", "")


def verify_amplitude_request(headers) -> bool:
    if not SECRET:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

Be clear about what this protects. A static header proves the sender knows the secret, nothing more. It does not cover the body, so it cannot detect a modified payload, and there is no timestamp, so a captured request can be replayed and will still pass. Serve the endpoint over HTTPS only, use a long random value, keep it out of logs, and rotate it if it might have leaked.

Make Amplitude webhooks production-ready. Hookdeck Event Gateway checks the secret header you configure, queues every forwarded event durably, and lets you retry and replay deliveries on your own schedule.

Amplitude webhook limitations and pain points

A secret header is all you get

The Problem: Nothing in an Amplitude request proves the body is unchanged or that the request is new. Anyone who learns the header value can send requests your endpoint will accept.

Why It Happens: The destination's only security setting is its custom headers. There is no signature, no timestamp and no published IP range to allowlist.

Workarounds:

  • Use a long random secret, sent only over HTTPS, and rotate it in Amplitude and in your environment together.
  • Treat forwarded data as untrusted input, and make handlers idempotent so a replayed request does no harm.

How Hookdeck Can Help: Hookdeck's Amplitude source compares the header you name with the exact value you enter. Hookdeck still answers the request with a 200, but a request with a missing or wrong value is recorded as failed verification and never becomes an event for your destination, so your service only sees authenticated traffic.

Retries are long, but there is no replay

The Problem: If your endpoint is down for more than about 4 hours, events are lost, and Amplitude offers no way to resend them.

Why It Happens: Amplitude makes one attempt, then nine more over 4 hours regardless of the error. Within each attempt it retries 5xx and 429 responses immediately, up to three attempts, and makes no further attempt after 4 seconds. The destination does not send on demand, so there is no backfill.

Workarounds:

  • Respond with a 2xx as soon as the request is authenticated and process later.
  • Alert on errors from your endpoint so an outage is noticed well within the 4-hour window.

How Hookdeck Can Help: Hookdeck accepts each request as it arrives and stores it, then delivers to your service with retries you configure. You can replay any event after a fix, so an outage on your side costs latency instead of data.

Bursts of 429 retries

The Problem: Rate-limiting the endpoint makes things worse: a 429 triggers immediate retries inside the same attempt.

Why It Happens: Amplitude streams as fast as it ingests, and its in-attempt retry policy treats 429 like a 5xx: an immediate retry after about 100 ms, with the wait doubling each time.

Workarounds:

  • Size your endpoint for your peak event rate, and filter the destination down to the events you need.
  • Push work onto a queue so the HTTP handler stays fast under load.

How Hookdeck Can Help: Hookdeck absorbs the burst and delivers to your service at a max delivery rate you set, so Amplitude never sees a 429 from you.

Wrong secret, silent loss

The Problem: After a secret rotation or a typo, every request fails authentication and nothing obvious tells you.

Why It Happens: The secret lives in two places, the destination's header row and your receiver's configuration, and nothing keeps them in sync. A Bearer prefix typed on one side and not the other is the usual cause.

Workarounds:

  • Store the whole header value as one string and compare it whole. Do not strip or add prefixes in code.
  • During a rotation, accept the old and the new value for a few hours, since retries already in flight carry the old one.

How Hookdeck Can Help: Hookdeck keeps every request, including the ones that fail verification, with full headers, so you can see exactly what Amplitude sent and compare it with what the source expects.

Best practices

Filter at the source

Select and filter events decides what Amplitude forwards. Send only the events your receiver acts on. It reduces load, cost and the blast radius of a bad deploy.

Treat the template as a contract

If you use a FreeMarker template, version it like an API schema. Keep the receiver tolerant of missing fields, since <#if> blocks drop fields that do not exist on the input, and add defaults with ! where an empty string would break your parser.

Deduplicate deliveries

Retries mean the same event can arrive more than once. With the default event format, store $insert_id or uuid and skip repeats. With a template, include one of those fields in the output so you have something to key on. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Amplitude's in-attempt retries stop after 4 seconds, so slow handlers turn into failed attempts. Authenticate, store the payload, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Handle both payload shapes on one route

If you forward events and users to the same URL, branch on the body: events have event_type, user updates do not. Log and acknowledge anything you do not recognise rather than returning an error that Amplitude will retry.

Conclusion

Amplitude's Webhook destination forwards your own events and user updates, in Amplitude's format or a FreeMarker template's, with no signature and no timestamp. Add a long random secret as a custom header, compare its whole value in constant time over HTTPS, filter down to the events you need, deduplicate on $insert_id or uuid, and acknowledge quickly so the 4-hour retry window is never your only safety net.

Hookdeck Event Gateway checks that header for you, stores every forwarded event, and lets you retry and replay on your own terms, so an outage on your side does not mean lost analytics data.

Get started with Hookdeck for free and handle Amplitude webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.