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:
| Plan | Active webhooks |
|---|---|
| Free | 0 (unavailable) |
| Starter | 3 |
| Scale | 10 |
| Enterprise | Custom |
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.
- Schedule —
Daily,Weekly, orMonthly. - Platform —
all,ios, orandroid. - 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:
| Schedule | Runs on | Period covered |
|---|---|---|
Daily | Every day | The previous day |
Weekly | Mondays | The previous 7 days |
Monthly | The 1st of each month | The 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
| Field | Meaning |
|---|---|
total_rows | Rows the export matched, before any limit |
returned_rows | Rows actually present in data |
truncated | true when data is only the first part of the result |
row_limit | The 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 (
Dailyrather thanWeekly/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.
Reading links rows
links deliveries contain two kinds of row, distinguished by param_key:
- Path rows (
param_keyis"") — 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 togetherclicks - 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.