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
conversion.createdA sale, lead or other conversion is recorded from a postback or the tracking script
conversion.updatedA conversion's status changes — pending to approved, or a refund or chargeback

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.