Why custom destinations
Custom destinations exist for the cases where a team needs TrackLayer’s normalization, enrichment, and delivery controls, but the receiver is not a mainstream ad or analytics platform. That often means internal analytics collectors, niche ad platforms, lightweight CDP ingestion endpoints, customer success workflows, or operational webhooks that post directly into Slack or PagerDuty. Instead of building a separate forwarding service for each case, the destination can live next to the rest of the routing layer.
The practical advantage is consistency. The same canonical event that powers a built-in platform integration can also be delivered to an internal endpoint with a custom schema, the same consent state, and the same retry semantics. That makes custom destinations a good fit for systems that need TrackLayer as the source of clean server-side events, but do not justify a full native integration yet.
Setup
Configuration happens from /settings/destinations. The page is a placeholder in the current product, but the setup model is straightforward: define the transport, narrow the event filter, shape the payload, choose retry behavior, and then test before exposing the receiver to live traffic.
Define the transport
Start in the placeholder destination screen at `/settings/destinations` and create a new custom HTTP destination. Enter the POST URL, choose the HTTP method, add any required custom headers as JSON, and set a signing secret if the receiver should verify authenticity. Most teams start with POST, but PUT is useful when the target system expects idempotent upserts keyed by event or customer ID.
POST URL
https://hooks.internal.example.com/tracklayer/events
HTTP method
POST
Custom headers JSON
{
"Authorization": "Bearer {{secrets.webhook_token}}",
"X-Environment": "production"
}
Signing secret
dest_live_9f3b27Apply an event filter
Do not send the full event firehose by default. Filter by `event_name`, `platform`, or `segment` so the destination receives only the traffic it can understand and afford to process. This is especially important for Slack channels, on-call workflows, niche ad APIs, or internal analytics collectors that care about only a narrow slice of the canonical stream.
{
"event_name": ["purchase", "refund_requested"],
"platform": ["shopify"],
"segment": ["vip_customers", "b2b_accounts"]
}Build the payload template
The payload template uses a Handlebars-like syntax so you can project TrackLayer fields into the schema the receiver expects. Variables such as `{{user_data.email_hash}}`, `{{event_name}}`, and `{{properties.order_id}}` let you reshape the canonical event without maintaining a separate transformation service. Keep templates explicit and versioned so the receiver contract stays readable.
{
"type": "{{event_name}}",
"timestamp": "{{occurred_at}}",
"user": {
"email_hash": "{{user_data.email_hash}}",
"external_id": "{{user_data.external_id}}"
},
"order_id": "{{properties.order_id}}",
"value": "{{properties.value}}",
"currency": "{{properties.currency}}"
}Set the retry policy
Custom endpoints vary widely in reliability, so retries should be destination-specific rather than global. Configure maximum attempts and exponential backoff to fit the receiving system. A Slack notification path may only need a few attempts, while a warehouse ingestion endpoint may justify a longer recovery window.
Attempts
5
Backoff
2s, 10s, 30s, 2m, 10mRun a synthetic test event
Before enabling live traffic, send a synthetic event through the destination and inspect the exact request body, headers, status code, and latency. Confirm the filter matched correctly, the template rendered the expected payload, the signature validates on the recipient, and the destination log shows the request as delivered instead of merely queued.
Synthetic event
purchase
User
qa+destinations@tracklayer.test
Expected outcome
2xx response and visible request in receiver logsTemplate examples
Slack channel
Useful for routing high-value operational or revenue events into a shared channel without building a separate worker.
{
"text": "TrackLayer {{event_name}} for order {{properties.order_id}}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*{{event_name}}* from {{platform}} — value {{properties.value}} {{properties.currency}}"
}
}
]
}Custom REST endpoint
A common choice for internal analytics collectors, fraud systems, loyalty engines, or downstream ETL services.
{
"event_id": "{{event_id}}",
"event_name": "{{event_name}}",
"segment": "{{segment}}",
"customer": {
"id": "{{user_data.external_id}}",
"email_hash": "{{user_data.email_hash}}"
},
"properties": {{json properties}}
}Mixpanel `/track`
For teams that want TrackLayer-normalized server events to also land in Mixpanel with its expected event wrapper.
{
"event": "{{event_name}}",
"properties": {
"time": "{{occurred_at}}",
"distinct_id": "{{user_data.external_id}}",
"$insert_id": "{{event_id}}",
"value": "{{properties.value}}",
"currency": "{{properties.currency}}"
}
}Signing + verification
When signing is enabled, TrackLayer computes an HMAC-SHA256 digest of the raw request body and sends it in the X-TrackLayer-Signature header. The receiver should verify the signature before trusting the payload. That means reading the raw bytes exactly as delivered, computing the digest with the shared secret, and comparing it with a timing-safe equality check.
This is the right default for internal APIs and third-party endpoints alike. IP allowlists are useful, but signatures protect against forged requests, replay confusion during debugging, and accidental processing of unauthenticated traffic in environments where multiple services can reach the same endpoint.
import crypto from "node:crypto";
export function verifyTrackLayerSignature(rawBody: string, header: string, secret: string) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = header.replace(/^sha256=/, "");
return (
expected.length === received.length &&
crypto.timingSafeEqual(
Buffer.from(expected, "utf8"),
Buffer.from(received, "utf8"),
)
);
}Rate limiting
Each custom destination should have its own maximum events-per-second setting so one fragile receiver does not throttle the whole workspace. The right limit depends on the receiver contract, the queue behind it, and how expensive retries become when the target starts returning `429` or timing out under load.
| Profile | Max rate | Typical use |
|---|---|---|
| Chatops webhook | 2 events/sec | Slack, Teams, PagerDuty-style notifications |
| Internal service | 25 events/sec | Internal APIs with queue-backed consumers |
| Warehouse collector | 100 events/sec | Batch-friendly ingestion endpoints |
Error handling + DLQ
A custom destination should assume that some failures are temporary and some are structural. Timeouts, short outages, and bursty `429` responses usually justify retries with backoff. Persistent `401`, `403`, or schema validation failures usually do not. The point of the retry policy is to give transient failures a recovery path without hiding a broken configuration for hours.
Once retry attempts are exhausted, the event should move to a dead-letter queue instead of disappearing. The DLQ is the operator safety net: it preserves the rendered payload, response code, error message, and destination state so someone can inspect the failure and decide whether to replay, edit the template, rotate a secret, or suppress that route entirely.
This is why idempotency matters on the receiving side. A replay from the DLQ should be safe. If the receiver keys writes on `event_id` or another stable identifier, operational recovery is a normal workflow instead of a duplication risk. Without that, every manual replay becomes a production gamble.
When to use vs built-in platform integration
Use a custom destination when the receiver is fundamentally “just an HTTP endpoint” and you need TrackLayer to deliver a shaped, signed request. Use a built-in integration when the platform has a first-class API contract, ongoing vendor quirks, or authentication flows that TrackLayer should manage directly.
Need a TrackLayer destination?
→ Built-in platform already exists
→ Use the built-in integration
→ No built-in platform exists
→ Receiver accepts generic HTTP
→ Use a custom destination
→ Receiver needs OAuth refreshes, complex object sync, or evolving vendor-specific schemas
→ Build or request a native integration insteadCommon questions
Should I use a custom destination for Meta, Google Ads, or TikTok?
Usually no. If TrackLayer already has a built-in integration for the platform, use it. Built-in destinations handle platform-specific auth, field mapping, diagnostics, retries, and API drift better than a generic HTTP template can.
Can one event go to both a built-in integration and a custom destination?
Yes. A canonical event can be routed to multiple destinations at once, which is useful when you want a standard ad-platform integration plus an internal analytics or alerting endpoint.
What happens if my template references a missing field?
The test event should catch most template mistakes before launch. In production, TrackLayer should log the rendered payload and mark the delivery as failed if the final body is invalid for the receiver.
Should the receiver be idempotent?
Yes. Retries, manual replays, and network ambiguity all happen in production. The receiver should use `event_id` or another stable key so the same TrackLayer event does not create duplicate records or actions.
Can I rotate the signing secret without downtime?
Yes, if the receiver supports a short overlap window. The practical pattern is to accept both the old and new secret briefly, then remove the old one after the destination has been updated.
Related implementation guides
Webhook delivery
Go deeper on signed webhooks, retries, replays, and idempotent consumers.
Read guide →Rate limiting
Design throughput controls that protect downstream APIs without dropping high-value events.
Read guide →Segment destination
See how TrackLayer fits into a broader event-routing stack when another collector sits upstream.
Read guide →