Skip to main content

Webhooks

Detour webhooks let you receive your analytics data on a recurring schedule, delivered automatically to an HTTPS endpoint you control. Instead of polling the dashboard or exporting manually, you register an endpoint once and Detour sends a signed JSON payload on each scheduled run.

Each webhook is tied to a specific app and a specific export type. You manage all of an organization's webhooks from the Webhooks section in the sidebar.


Availability

Webhooks are a paid feature. The number of active webhooks you can run depends on your plan:

PlanActive webhooks
Free0 (unavailable)
Starter3
Scale10
EnterpriseCustom

For full plan details, see Billing & Payments.

  • If you try to create a webhook beyond your active limit, it is saved but created paused (inactive) — activate it later after freeing up a slot.
  • If you downgrade to a plan with a lower limit, the oldest active webhooks are automatically paused to fit the new limit.

Creating a webhook

When creating a webhook you configure:

  • App — which app's data to export.
  • Endpoint URL — where the payload is delivered. Must be a public HTTPS URL on the default port (443). Endpoints that resolve to private/internal addresses are rejected.
  • Export type — the dataset to send:
    • Overview — aggregated performance (link stats, organic vs non-organic installs, retention, top events).
    • Links — link-level breakdown (paths and query parameters).
    • Events — SDK event activity.
  • ScheduleDaily, Weekly, or Monthly.
  • Platformall, ios, or android.
  • Filters — optional, to narrow the exported data.

When the webhook is created, a signing secret is shown once. Store it securely — it cannot be retrieved again later (you can rotate it if lost, see Managing webhooks).


Delivery schedule

Deliveries run on a fixed daily cycle (around 08:00 UTC). Each delivery covers the period that just closed:

ScheduleRuns onPeriod covered
DailyEvery dayThe previous day
WeeklyMondaysThe previous 7 days
MonthlyThe 1st of each monthThe previous calendar month

All period boundaries are in UTC.


Payload format

Each delivery is an HTTP POST with a JSON body:

{
"webhook_id": "<your webhook id>",
"type": "overview | links | events",
"app_id": "<your app id>",
"period": {
"start": "2026-05-01T00:00:00.000Z",
"end": "2026-06-01T00:00:00.000Z"
},
"delivered_at": "2026-06-01T08:00:00.000Z",
"meta": {
"total_rows": 4821,
"returned_rows": 4821,
"truncated": false,
"row_limit": 10000
},
"data": [
/* rows for the selected export type */
]
}

The shape of each row in data matches the chosen export type (the same rows you get from a manual export of that view).

meta

FieldMeaning
total_rowsRows the export matched, before any limit
returned_rowsRows actually present in data
truncatedtrue when data is only the first part of the result
row_limitThe per-delivery row cap in force

meta is additive: data keeps exactly the shape and position it has always had, so an existing integration that only reads data needs no changes.


Row limits

A single delivery carries at most 10,000 rows. This is a limit on the HTTP body Detour sends you, not on the data itself.

Check meta.truncated on each delivery. When it is true, data holds the first row_limit rows of a larger result and meta.total_rows tells you how many there were in total.

If your deliveries are being truncated:

  • Narrow the webhook with filters so fewer clicks are in scope, or
  • switch to a shorter schedule (Daily rather than Weekly/Monthly) so each delivery covers a smaller window, or
  • pull the full dataset from the analytics export API, which supports paging and parameter-level filtering.

links deliveries contain two kinds of row, distinguished by param_key:

  • Path rows (param_key is "") — totals for a date, short link and path.
  • Parameter rows — the breakdown of one query parameter value within that path.

Already-installed opens

An already-installed open is a tap on one of your links where the app was already installed, so iOS or Android opened it straight in the app rather than a browser — see Universal Links and App Links. The SDK reports them from inside the app.

They land on the path row of the short link that produced them, and they count as clicks:

2026-05-14 | https://go.link/s1 | /promo | ... | clicks=4 | already_installed_opens=1

So on a path row:

  • clicks — all opens, web and already-installed together
  • clicks - already_installed_opens — web opens only

short_link is empty when the tap was on a plain deep link rather than a short link, and on opens recorded before Detour began attributing them.

These opens are attributed at path level only, so they never appear as parameter rows. Filters still apply to them normally.

Totalled this way, a links delivery reconciles with an overview delivery and with the dashboard for the same app, period and platform.


Verifying the signature

Every request includes an X-Webhook-Signature header so you can confirm the payload came from Detour and wasn't tampered with:

X-Webhook-Signature: sha256=<hex digest>

The digest is an HMAC-SHA256 of the raw request body, keyed with your webhook's signing secret. Compute the same HMAC on your side and compare:

import { createHmac, timingSafeEqual } from "crypto";

function isValidSignature(rawBody, header, signingSecret) {
const expected = createHmac("sha256", signingSecret)
.update(rawBody)
.digest("hex");
const received = (header ?? "").replace(/^sha256=/, "");
const a = Buffer.from(expected);
const b = Buffer.from(received);
return a.length === b.length && timingSafeEqual(a, b);
}

Always compute the HMAC over the raw, unparsed request body. Re-serializing the JSON can change bytes (key order, whitespace) and break the comparison.


Delivery behavior

  • Detour only follows the configured URL — redirects are not followed.
  • The endpoint must respond with a 2xx status within 10 seconds for the delivery to count as successful.
  • Failed deliveries are not retried within the same cycle. The next attempt is the next scheduled run. Make sure your endpoint is reachable at delivery time.
  • Each cycle has a fixed processing budget. In the rare case that an unusually heavy export cannot be produced within it, that webhook is recorded as failed for the cycle and picked up again on the next scheduled run — narrowing the webhook's filters or date window makes this less likely.
  • The dashboard shows the last successful delivery time for each webhook.

Managing webhooks

From the Webhooks section you can:

  • Pause / resume a webhook. Resuming is subject to your plan's active-webhook limit — pause another webhook first if you're at the limit.
  • Rotate the signing secret. A new secret is shown once and the previous one stops being valid immediately, so update your endpoint before rotating.
  • Delete a webhook.

There is no in-place edit for a webhook's URL, schedule, export type, or platform — to change those, delete the webhook and create a new one.