Webhooks API

Receive LinkTrail installs and events as signed webhooks: register an endpoint, the event types, the payload, and verifying the X-LinkTrail-Signature HMAC.

A webhook pushes installs and in-app events to your server as they happen. Webhooks are on the Growth and Scale plans (other plans get 402), and reading or managing them needs developer, admin or owner.

Note

Every endpoint on this page needs Authorization: Bearer <accessToken> and X-Workspace-Id: <id> — see Authentication. Paths are relative to https://backend.linktrail.io/v1.

MethodPathWhat it does
GET/webhooksList endpoints (never includes the secret)
POST/webhooksRegister an endpoint; returns the signing secret once
GET/webhooks/:webhookIdLoad one
PATCH/webhooks/:webhookIdChange the URL, the events or enabled
DELETE/webhooks/:webhookIdRemove it
POST/webhooks/:webhookId/testSend a sample payload (10 a minute per workspace)
POST /v1/webhooks — requestJSON
{ "url": "https://example.com/hooks/linktrail", "events": ["install.attributed"] }
201 responseJSON
{ "id": 1, "url": "https://example.com/hooks/linktrail", "events": ["install.attributed"],
  "enabled": true, "consecutiveFailures": 0, "lastDeliveryAt": null, "lastError": null,
  "secret": "whsec_…" }
Important

Store secret when you create the webhook. It is never returned again, and it is what proves a delivery came from LinkTrail.

Events

EventSent when
install.recordedAny install is recorded
install.attributedThat install matched a click
install.fraudThe install was flagged as fraud: click injection on every plan; on Scale also click flooding, a country mismatch, IP velocity or a repeated device
event.recordedA post-install event (purchase, sign-up, custom) is recorded

An empty events array subscribes to all of them. Only installs and events recorded with consent exist to be sent.

Payload

POST to your endpointJSON
{ "event": "install.attributed",
  "data": { "id": 91, "platform": "iOS", "country": "GB", "linkId": 4, "campaignId": 2,
            "attributed": true, "matchType": "deterministic", "reengagement": false,
            "fraud": false, "createdAt": "2026-06-28T10:00:00Z" },
  "deliveredAt": "2026-06-28T10:00:00Z" }
HeaderValue
X-LinkTrail-EventThe event name
X-LinkTrail-TimestampUnix time in seconds
X-LinkTrail-Signaturesha256=<hex>

Verify the signature

The signature is an HMAC-SHA256 of <timestamp>.<raw body> keyed with your secret, in hex. Compute it over the raw request body — before any JSON parsing — compare in constant time, and reject a timestamp older than a few minutes. The timestamp is inside the signed string, so a captured delivery cannot be replayed with a fresh one.

verify-webhook.ts (Node.js)TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a string or Buffer, not parsed JSON).
export function verifyLinkTrailWebhook(
  rawBody: string | Buffer,
  headers: Record<string, string | undefined>,
  secret: string, // whsec_…
  toleranceSeconds = 300,
): boolean {
  const timestamp = headers["x-linktrail-timestamp"];
  const signature = headers["x-linktrail-signature"];
  if (!timestamp || !signature?.startsWith("sha256=")) return false;

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const given = signature.slice("sha256=".length);
  return (
    given.length === expected.length &&
    timingSafeEqual(Buffer.from(given), Buffer.from(expected))
  );
}

Delivery rules

  • The URL must be public https. Private, loopback and link-local addresses are rejected with 400, and redirects are not followed — a 3xx counts as a failed delivery.
  • Answer with a 2xx within 5 seconds. A delivery is sent once and is not retried: a slow or failing endpoint misses that notification, never the install.
  • After 20 consecutive failures the webhook is disabled. PATCH it with { "enabled": true } to turn it back on and reset the count.
  • POST /webhooks/:webhookId/test answers { "delivered": true|false, "lastError": null|"…" }. A 429 from it means nothing was sent.
  • Payloads never carry the raw IP address.