Webhooks

Get a signed HTTP POST the moment something happens, instead of polling for it. Available on Agency and Business.

Events

EventFires when
click.createdSomeone clicks a tracked link or loads a pixel
link.createdA link is created, in the dashboard or via the API
link.updatedA link's name, destination, slug or state changes
link.deletedA link is deleted

click.created is a firehose. A busy link can produce thousands of events an hour. Subscribe to it only if your endpoint can absorb that, and expect bursts rather than a steady trickle.

Payload

Every delivery has the same envelope. id is stable per delivery — use it to make your handler idempotent, because a retry reuses it.

{
  "id": "5a1f8c2e-...",
  "event": "click.created",
  "created_at": "2026-08-28T09:14:22.104Z",
  "data": {
    "click_id": "c_9f2...",
    "link_id": "3ddb1d53-...",
    "slug": "summer",
    "created_at": "2026-08-28T09:14:22.061Z",
    "is_unique": true,
    "country": "Germany",
    "country_code": "DE",
    "city": "Berlin",
    "device_type": "Mobile",
    "os": "iOS",
    "browser": "Safari",
    "referrer": null,
    "utm_source": "newsletter",
    "utm_medium": "email",
    "utm_campaign": "august"
  }
}

Click payloads carry no IP address and no precise coordinates, even when we hold them. link.* payloads carry the link's id, slug, name, type, destination and active state.

Headers

X-Tracklink-Event:     click.created
X-Tracklink-Delivery:  5a1f8c2e-...        # stable across retries
X-Tracklink-Signature: t=1756371262,v1=9c4f...
User-Agent:            TrackLink-Webhooks/1.0

Verify the signature

Your endpoint is a public URL. Without verification, anyone who learns it can post fake events to you. Sign over `${t}.${rawBody}` with the signing secret shown once when you created the endpoint.

import { createHmac, timingSafeEqual } from "crypto";

// rawBody must be the raw request STRING. If your framework has already
// parsed it into an object, re-serialising will not reproduce the bytes
// that were signed, and every signature will fail.
export function verify(rawBody, header, secret) {
  const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header || "");
  if (!m) return false;

  // Reject anything older than five minutes. The timestamp is inside the
  // signed material precisely so a captured request cannot be replayed
  // at you later.
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${m[1]}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(m[2]);
  // Constant-time: a plain === leaks how much of the digest matched.
  return a.length === b.length && timingSafeEqual(a, b);
}

In Express, get the raw body with express.raw({ type: 'application/json' }) on that route. In Next.js route handlers, use await req.text().

Responding

Return any 2xx as soon as you have the payload — do the work afterwards. We wait 10 seconds, and a slow endpoint delays your own later events.

You returnWe do
2xxDone. Nothing is recorded.
4xx (not 429)Treated as permanent — you understood and rejected it. No retry.
5xx, 429, timeoutRetried 5 times with exponential backoff from 10s.

When an endpoint dies

After 15 consecutive deliveries exhaust their retries, the endpoint is disabled automatically and stops receiving events. A single blip will not do it — only an exhausted delivery counts, so a brief outage costs you nothing. Fix the endpoint and press Enable on the Developers page; that clears the counter.

Failed deliveries are listed per endpoint with the status code and response body, for 7 days. Successful ones are not recorded — at click volume that log would be the largest thing we store.

Endpoint requirements

  • Must be https.
  • Must be publicly reachable. Private, loopback and link-local addresses are rejected — both when you save the endpoint and again at the moment we connect, so a hostname that later resolves to an internal address is refused.
  • Redirects are not followed. Give us the final URL.
  • Up to 10 endpoints per workspace.