Skip to main content
GUIDE · WEBHOOKS10 min read

TrackLayer webhooks: subscribe to events, deliveries, anomalies

A production guide for consuming TrackLayer webhooks with durable payload validation, signature verification, retry-aware endpoint design, idempotent processing, and fast operational feedback when deliveries, alerts, anomalies, exports, or platform state changes occur.

Topics

What can trigger a webhook

TrackLayer webhooks are operational notifications, not just event copies. They exist so another system can react when routing succeeds, fails, drifts, or produces a file that is ready to consume. In practice, that means you can wire webhooks into chat, incident response, internal dashboards, support tooling, or batch workflows without adding a polling job around the TrackLayer API.

The key design rule is to treat each webhook type as its own contract. Some messages describe delivery outcomes, some describe monitoring state, and some describe asynchronous job completion. Your endpoint can be shared, but each topic should still have dedicated validation and ownership.

TypeWhen it firesTypical `data` fields
event.deliveredA routed event reaches a destination successfully after TrackLayer validates and dispatches the payload.destination, event_id, destination_event_id, latency_ms
event.failedA destination delivery fails after an API rejection, timeout, auth error, schema validation error, or exhausted retry cycle.destination, event_id, error_code, http_status, retry_count
alert.triggeredA configured operational rule crosses its threshold, such as error rate spikes, queue lag, or a destination outage.alert_id, severity, metric, threshold, window_minutes
anomaly.detectedTrackLayer detects unusual movement in volume, conversion rate, latency, or another monitored signal compared with baseline behavior.anomaly_id, metric, expected_value, observed_value, confidence
data.export.readyAn asynchronous export job finishes and the file or signed URL is ready for retrieval by another system.export_id, format, row_count, download_url, expires_at
platform.disconnectedA connected platform loses authorization or becomes unreachable, putting event delivery or sync reliability at risk.platform, account_id, disconnected_at, reason, action_url
Schema

Payload structure

Every TrackLayer webhook has the same outer envelope even though the nested `data` payload changes by topic. That consistency is what lets you build one receiver, one signature verification layer, one dedup store, and one audit log format. Start by validating the canonical fields at the top level before you route deeper into the message.

id

A globally unique webhook identifier. Use this as the dedup key in your consumer, replay tooling, and delivery logs.

type

The canonical event name for the webhook topic, such as `event.delivered` or `anomaly.detected`. Route on this field before inspecting nested payload data.

created_at

The UTC ISO-8601 timestamp for when TrackLayer generated the webhook. Keep this separate from timestamps inside `data` that may describe the underlying business event.

data

The typed payload body for the webhook. The shape varies by `type`, but consumers should still parse it through explicit schema validation.

{
  "id": "wh_01HSV5DZJ7M9J9J6XQ4N8Q5K4A",
  "type": "event.delivered",
  "created_at": "2026-04-23T09:41:22Z",
  "data": {
    "destination": "meta_capi",
    "workspace_id": "ws_7k2p9f",
    "event_id": "evt_01HSV5DW7QG9M2M7T8V9P4Z6A1",
    "destination_event_id": "meta:7019284411",
    "status": "delivered",
    "attempt": 1,
    "latency_ms": 184,
    "received_at": "2026-04-23T09:41:21Z",
    "delivered_at": "2026-04-23T09:41:22Z",
    "customer": {
      "external_id": "cust_10492",
      "email_hash": "9c2f6b9c7d56d6b52d3ff7d3b4c6c1ef"
    },
    "properties": {
      "event_name": "Purchase",
      "value": 129.95,
      "currency": "EUR",
      "order_id": "10492"
    }
  }
}
Security

Signing + verification

TrackLayer signs every webhook with HMAC-SHA256 and sends the digest in the `X-TrackLayer-Signature` header. The verification input is the raw request body exactly as received. That detail matters because any middleware that parses and re-serializes JSON can change whitespace, key order, or encoding and break a valid signature check.

The normal handler sequence is: capture raw bytes, compute the HMAC using your shared secret, compare with the header using a timing-safe comparison, reject on mismatch, then parse and route the body. If your framework makes raw body access awkward, solve that first. Signature verification that runs against a mutated payload is worse than no verification because it fails unpredictably in production.

Node.js

import crypto from "node:crypto";

function verifyTrackLayerSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = signatureHeader?.replace(/^sha256=/, "") ?? "";

  return (
    expected.length === received.length &&
    crypto.timingSafeEqual(
      Buffer.from(expected, "utf8"),
      Buffer.from(received, "utf8"),
    )
  );
}

Python

import hashlib
import hmac


def verify_tracklayer_signature(raw_body: bytes, signature_header: str, secret: str):
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    received = (signature_header or "").removeprefix("sha256=")
    return hmac.compare_digest(expected, received)
Delivery

Retry policy

Webhooks should be designed with the assumption that downstream systems are occasionally slow, unavailable, or mid-deploy. TrackLayer retries failed deliveries with exponential backoff. The receiving side should therefore distinguish between transient problems and permanent schema or auth failures, because every non 2xx response extends the total recovery window.

The table below shows the backoff schedule. After the exponential cycle completes, the message moves to a dead-letter queue for operator review or replay. That is another reason idempotency matters: a manual replay should be operationally safe.

AttemptWaitTotal elapsed
11s1s
22s3s
34s7s
48s15s
516s31s
632s63s
764s127s
8128s255s
Final backoff256s511s → DLQ
Correctness

Idempotency

Webhook delivery is at-least-once by design. That means your consumer must tolerate duplicate delivery without producing duplicate side effects. The correct dedup key is the top-level `id` field, not a combination of timestamps, types, or nested data values that may be harder to reason about later.

A practical pattern is to write the webhook `id` into a durable store with status fields such as `received`, `processing`, `processed`, and `failed`. On a duplicate delivery, short-circuit and return success if the message was already accepted or completed. That way retries, replay tooling, and race conditions collapse to one outcome even when your handler is scaled across multiple workers.

dedup_key = webhook.id

if store.exists(dedup_key):
    return 200

store.insert(dedup_key, status="received")
queue.enqueue(webhook)
return 202
Patterns

5 worked examples

Example 1

Slack alert on `event.failed`

Post a concise failure summary to a routing or data-quality channel when a high-value destination rejects events. Include destination name, error class, retry count, and a deep link back into TrackLayer so the operator can replay or mute quickly.

if (webhook.type === "event.failed") {
  await slack.chat.postMessage({
    channel: "#ops-events",
    text: [
      "TrackLayer delivery failed",
      "destination: " + webhook.data.destination,
      "event_id: " + webhook.data.event_id,
      "error: " + webhook.data.error_code,
      "retries: " + webhook.data.retry_count,
    ].join("\n"),
  });
}
Example 2

PagerDuty incident on `alert.triggered`

Escalate only the alerts that imply customer-visible impact, such as sustained queue lag or a destination-wide outage. Map TrackLayer severities to PagerDuty urgency so minor warnings stay informational while hard failures page the on-call rotation.

if (webhook.type === "alert.triggered") {
  await pagerduty.incidents.create({
    title: webhook.data.title,
    urgency: webhook.data.severity === "critical" ? "high" : "low",
    details: {
      metric: webhook.data.metric,
      observed: webhook.data.observed_value,
      threshold: webhook.data.threshold,
    },
  });
}
Example 3

Custom notification on `anomaly.detected`

Send a product or growth team notification when anomaly detection sees conversion collapse, missing event volume, or suspicious traffic spikes. The useful pattern is not just forwarding the alert, but enriching it with your own ownership metadata and dashboards.

if (webhook.type === "anomaly.detected") {
  await notifications.create({
    audience: "growth-team",
    title: "TrackLayer anomaly detected",
    body: `${webhook.data.metric} moved from ${webhook.data.expected_value} to ${webhook.data.observed_value}`,
    deepLink: "/dashboards/anomalies/" + webhook.data.anomaly_id,
  });
}
Example 4

Email on `data.export.ready`

When exports complete asynchronously, email the requester a signed download link and expiration window. That keeps the export worker decoupled from the user-facing product while still giving the customer a clear delivery path.

if (webhook.type === "data.export.ready") {
  await email.send({
    to: webhook.data.requested_by_email,
    subject: "Your TrackLayer export is ready",
    html: `Download: <a href="${webhook.data.download_url}">export</a><br/>Expires: ${webhook.data.expires_at}`,
  });
}
Example 5

Healthcheck heartbeat on `platform.connected`

Some teams emit a separate internal `platform.connected` event whenever a destination recovers or an authorization handshake succeeds. Use that event to mark a heartbeat green in your monitoring stack so disconnections and recoveries are both visible.

if (webhook.type === "platform.connected") {
  await healthchecks.ping({
    service: webhook.data.platform,
    status: "up",
    accountId: webhook.data.account_id,
  });
}
Development

Local testing

Local webhook work is mostly about inspecting exact requests and iterating quickly on signature verification, schema handling, and response behavior. You do not need a full staging deployment just to prove the integration path. A tunnel or request bin is usually enough for the first pass.

ngrok

Expose a local server over HTTPS with a stable inspect UI. Useful when you need to replay requests, inspect raw headers, and verify signatures against the exact request body.

Cloudflare Tunnel

Create a secure tunnel without opening inbound firewall rules. Good for teams already using Cloudflare infrastructure or wanting a lightweight dev ingress path.

Hookbin

Receive webhooks without writing code first. Best for quick payload inspection, schema confirmation, or demonstrating a new webhook topic before implementing a full consumer.

Use ngrok when you want raw replay and a mature inspect UI. Use Cloudflare Tunnel if your team already routes development ingress through Cloudflare. Use Hookbin before you write code, when the immediate need is to inspect headers, payload shape, and event sequencing during development.

Diagnostics

Troubleshooting

401 signature verification failed

Verify against the raw request body, not a re-serialized JSON object. Shared secret mismatches, whitespace changes, framework body parsing, and clock-independent string mutations are the common causes.

Timeouts or repeated retries

Return a 2xx response quickly and push heavy work into a queue. Webhook handlers should validate, dedup, persist, and acknowledge before doing slow downstream API calls.

429 rate limit from your consumer

If your endpoint or downstream API is throttling, absorb the burst with a queue and concurrency control. Do not make TrackLayer retries your primary rate-limiting strategy.

Body parsing errors

Confirm `Content-Type: application/json`, preserve the exact bytes for signature verification, and log parse failures with request size plus a truncated payload preview for debugging.

Event replay duplicates

Manual replays and automatic retries must hit idempotent code paths. Store the webhook `id` with processing status so duplicate deliveries short-circuit safely instead of creating duplicate incidents, emails, or records.

FAQ

Common questions

Should the webhook endpoint do business logic directly?

Usually no. The safer pattern is to authenticate the request, validate the schema, persist or enqueue the job, and return immediately. That keeps delivery latency predictable and isolates retries from slow downstream systems.

Do I verify the signature before JSON parsing?

You usually capture the raw body first, compute the HMAC on those bytes, compare it with `X-TrackLayer-Signature`, and only then trust the parsed JSON. Many framework bugs come from verifying a reconstructed object instead of the original payload.

What status code should my endpoint return?

Return a 2xx response once you have authenticated, validated, and safely accepted the message for processing. Non-2xx responses tell TrackLayer to retry according to the delivery policy.

How long should I keep dedup records?

Keep them at least as long as the maximum retry plus replay window you support operationally. Many teams keep webhook IDs for several days so delayed retries and manual replays still collapse correctly.

Can one endpoint receive all webhook types?

Yes. A single endpoint with routing on `type` is common, as long as each type has explicit schema validation and ownership. Separate endpoints only make sense if different teams or trust boundaries need isolation.

Next reads

Related implementation guides

We use essential cookies to keep the site secure and functional. Analytics and third-party tags run only with your consent. See our Cookie Policy.

We use essential cookies to keep the site secure and functional. Analytics and third-party tags run only with your consent. See our Cookie Policy.