Platform API
The Platform API allows you to programmatically manage your Detour resources and retrieve analytics data. All endpoints require authentication using a secret API key.
Authentication
All Platform API endpoints require authentication via Bearer token and application identification.
API Key Types
Detour provides two types of API keys:
| Key Type | Purpose | Use Case |
|---|---|---|
| Publishable | Identify your app, used in SDKs | Client-side SDK implementations |
| Secret | Full platform management & administration | Server-side API calls, automation |
Authentication Headers
Include the following headers in all API requests:
Authorization: Bearer <SECRET_API_KEY>
X-App-ID: <YOUR_APP_ID>
Example:
curl -X GET https://api.godetour.link/api/manage/short-links \
-H "Authorization: Bearer 1234567890abc..." \
-H "X-App-ID: your-app-id"
Secret keys must be stored securely on your server. Never expose them in client-side code or commit them to version control.
Endpoints
Short Links Management
GET /api/manage/short-links
Retrieve all short links for your app.
Authentication: Requires secret API key
Query Parameters: None
Response:
[
{
"id": "short_link_uuid",
"hash": "abc123",
"url": "https://acme-corp.godetour.link/abc123",
"parameters": "{\"campaign\":\"summer_sale\"}",
"name": "Summer Sale Campaign",
"link_id": "link_uuid",
"created_at": "2026-06-16T10:30:00.000Z"
}
]
Status Codes:
200 OK— Successfully retrieved short links401 Unauthorized— Invalid or missing API key429 Too Many Requests— Rate limit exceeded500 Internal Server Error— Server error
Example:
curl -X GET https://api.godetour.link/api/manage/short-links \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app"
POST /api/manage/short-links
Create a new short link for your app.
Authentication: Requires secret API key
Request Body:
{
"parameters": "{\"campaign\":\"summer_sale\",\"utm_source\":\"email\"}",
"name": "Summer Sale Email Campaign"
}
Required Fields:
parameters(string) — JSON string of custom parameters (cannot be empty)
Optional Fields:
name(string) — User-friendly name for the short link
Response:
{
"id": "short_link_uuid",
"hash": "abc123",
"url": "https://acme-corp.godetour.link/abc123",
"parameters": "{\"campaign\":\"summer_sale\",\"utm_source\":\"email\"}",
"name": "Summer Sale Email Campaign",
"link_id": "link_uuid",
"created_at": "2026-06-16T10:35:00.000Z"
}
Status Codes:
201 Created— Short link successfully created400 Bad Request— Invalid request body (missing required fields)401 Unauthorized— Invalid or missing API key402 Payment Required— Short link limit reached on free plan404 Not Found— Link not found for this app429 Too Many Requests— Rate limit exceeded500 Internal Server Error— Server error
Plan Limits:
| Plan | Short Links Limit |
|---|---|
| Free | 50 |
| Paid | Unlimited |
Examples:
Create a short link:
curl -X POST https://api.godetour.link/api/manage/short-links \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app" \
-H "Content-Type: application/json" \
-d '{
"parameters": "{\"campaign\":\"summer_sale\"}",
"name": "Summer Sale"
}'
PATCH /api/manage/short-links
Update an existing short link's parameters and name.
Authentication: Requires secret API key
Request Body:
{
"id": "short_link_uuid",
"parameters": "{\"campaign\":\"fall_sale\"}",
"name": "Fall Sale Campaign"
}
Required Fields:
id(string) — UUID of the short link to updateparameters(string) — Updated JSON string of custom parameters
Optional Fields:
name(string) — Updated user-friendly name
Response:
{
"id": "short_link_uuid",
"hash": "abc123",
"url": "https://acme-corp.godetour.link/abc123",
"parameters": "{\"campaign\":\"fall_sale\"}",
"name": "Fall Sale Campaign",
"link_id": "link_uuid",
"created_at": "2026-06-16T10:35:00.000Z"
}
Status Codes:
200 OK— Short link successfully updated400 Bad Request— Invalid request body (missing required fields)401 Unauthorized— Invalid or missing API key404 Not Found— Short link not found429 Too Many Requests— Rate limit exceeded500 Internal Server Error— Server error
Example:
curl -X PATCH https://api.godetour.link/api/manage/short-links \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app" \
-H "Content-Type: application/json" \
-d '{
"id": "short_link_uuid",
"parameters": "{\"campaign\":\"fall_sale\"}",
"name": "Fall Sale Campaign"
}'
Link Management
GET /api/manage/links
Retrieve the main link information for your app, including URL, redirect settings, and parameter configuration.
Authentication: Requires secret API key
Query Parameters: None
Response:
{
"id": "link_uuid",
"url": "https://example.com",
"redirect_url": "https://fallback.example.com",
"params_pass_strategy": "all",
"specific_params": null,
"created_at": "2026-06-16T10:00:00.000Z",
"app_hash": "x9y8z7"
}
Fields:
id(string) — UUID of the linkurl(string) — Primary redirect URLredirect_url(string) — Fallback redirect URLparams_pass_strategy(string) — How parameters are passed:"all","none", or"specific"specific_params(array | null) — If set, only these parameters are passed throughcreated_at(string) — Link creation timestamp (ISO 8601)app_hash(string) — Hash identifier used in short link URLs
Status Codes:
200 OK— Successfully retrieved link401 Unauthorized— Invalid or missing API key404 Not Found— Link not found for this app429 Too Many Requests— Rate limit exceeded500 Internal Server Error— Server error
Example:
curl -X GET https://api.godetour.link/api/manage/links \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app"
Analytics
GET /api/analytics/stats
Retrieve analytics data for your app with various export formats and filtering options.
Authentication: Requires secret API key
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | — | Data type: "overview", "links", or "events" |
startDate | string | Yes | — | Start of date range (ISO 8601, e.g., 2026-06-01T00:00:00Z) |
endDate | string | Yes | — | End of date range (ISO 8601, e.g., 2026-06-30T23:59:59Z). Max range: 120 days |
format | string | No | json | Export format: "json", "csv" or "xlsx" |
platform | string | No | all | Filter by platform: "all", "ios", or "android" |
linkId | string | No | — | UUID of a specific link to scope the query. Defaults to the main app link (relevant for "links" and "overview" types; ignored for "events") |
limit | string | No | 1000 | Rows per page: an integer from 1 to 1000, or "all" for the complete set. links and events only. See Pagination |
offset | number | No | 0 | Rows to skip. links and events only, and not accepted together with limit=all. See Pagination |
envelope | string | No | — | "true" or "1" wraps a JSON response as { meta, rows }. See Pagination |
Additional parameters narrow the data itself — see Filtering.
Plan Requirements:
type | Required Plan |
|---|---|
overview | Free |
links | Paid |
events | Paid |
Response (type=links):
[
{
"date": "2026-06-16",
"short_link": "https://acme-corp.godetour.link/abc123",
"path": "/checkout",
"param_key": "utm_source",
"param_value": "facebook",
"clicks": 128,
"matched_installs": 12,
"fallbacks": 3,
"already_installed_opens": 0
}
]
Two kinds of row share this shape:
- Path rows (
param_keyandparam_valueare"") — the totals for that date, short link and path. - Parameter rows — the breakdown of a single parameter value inside that path.
Parameter rows are a breakdown, not additional traffic, so their clicks add up to more than the path row whenever a URL carries several parameters. Sum path rows only when you want totals.
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 these from inside the app.
They land on the path row of the short link that produced them, and they count as clicks:
2026-06-16 | https://acme-corp.godetour.link/abc123 | /checkout | ... | clicks=4 | already_installed_opens=1
So on a path row clicks covers both kinds; use clicks - already_installed_opens for web opens only. These opens are attributed at path level only and never appear as parameter rows, though filters still apply to them. 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.
Totalled this way, type=links reconciles with type=overview and with the dashboard for the same app, period and platform.
Status Codes:
200 OK— Analytics data successfully retrieved400 Bad Request— Missing or invalidtype, invalid filter syntax, invalidlimit/offset, or date range exceeds 120 days401 Unauthorized— Invalid or missing API key402 Payment Required— This data type requires a paid plan403 Forbidden— The requested link is not accessible with this API key429 Too Many Requests— Rate limit exceeded500 Internal Server Error— Server error503 Service Unavailable— The query took too long; narrow the date range or the parameters returned
Every error response from this endpoint carries a stable code alongside the message, so you can branch on code rather than parsing error:
code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Missing or invalid API key, or unknown app |
payment_required | 402 | This data type requires a paid plan |
forbidden | 403 | The app or link is not accessible with this key |
not_found | 404 | No active app for this X-App-ID |
invalid_parameter | 400 | A parameter was malformed or out of range — the message says which |
invalid_type | 400 | Unknown type |
rate_limited | 429 | Rate limit exceeded — see the Retry-After header |
query_timeout | 503 | The query exceeded the database time limit |
export_failed | 500 | Unexpected server error while building the response |
internal_error | 500 | Unexpected server error before the query ran |
{
"error": "Analytics query timed out",
"code": "query_timeout",
"hint": "Narrow the date range, or use paramKeys/excludeParams to drop high-cardinality parameters."
}
Examples:
Get overview analytics:
curl -X GET "https://api.godetour.link/api/analytics/stats?type=overview&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app"
Export link analytics as CSV (paid):
curl -X GET "https://api.godetour.link/api/analytics/stats?type=links&format=csv&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app" \
-o analytics.csv
Filter by platform:
curl -X GET "https://api.godetour.link/api/analytics/stats?type=events&platform=ios&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app"
Filtering
Link analytics can be narrowed in two independent ways. They do different jobs and they combine freely.
| Click filters | Row filters | |
|---|---|---|
| Parameters | filters, filterPath, filterParamName, filterParamValue, filterParam.<key> | paramKeys, excludeParams |
| Decides | Which clicks are counted | Which breakdown rows come back |
| Changes click totals? | Yes | No |
| Can exclude? | No | Yes |
| Works on | Paths, parameter names, parameter values, key/value pairs | Parameter names |
Both apply to type=links only. type=events aggregates by event name and platform and never sees a URL, and type=overview returns per-day totals unfiltered. Sending any of these parameters with those types returns 400 rather than ignoring them — a silently dropped filter is indistinguishable from a genuine "no clicks matched".
Click filters
Each click filter is a test against the click's URL. Values inside one filter are OR'd; separate filters are AND'd. Operators are is (default), contains and starts_with. There is no "not equal" operator — use row filters to exclude.
All three match your value literally; there is no wildcard or pattern syntax.
One rule covers case: filter values are compared case-insensitively; only the key you name in filterParam.<key>, paramKeys and excludeParams is exact. So filterParam.utm_source=facebook matches FaceBook, while filterParam.UTM_Source=facebook matches nothing — the exact ones select the same key the breakdown groups by.
| What you want to match | Parameter | Operator parameter |
|---|---|---|
| Path | filterPath=/checkout | filterPathOp |
| Parameter is present | filterParamName=gclid | filterParamNameOp |
| Any parameter's value | filterParamValue=summer | filterParamValueOp |
| A specific key = value | filterParam.utm_source=facebook | filterParamOp |
# Clicks whose path starts with /checkout
?filterPath=/checkout&filterPathOp=starts_with
# Clicks that carry a gclid at all
?filterParamName=gclid
# Facebook OR Google traffic, on the spring campaign (AND across, OR within)
?filterParam.utm_source=facebook,google&filterParam.utm_campaign=spring
filterParam.<key> can be repeated for as many parameter names as you need. Repeating the same key merges its values into one OR'd set, so filterParam.utm_source=a&filterParam.utm_source=b means the same as filterParam.utm_source=a,b.
Filters can also be passed as a URL-encoded JSON array in filters, which is useful when you build them programmatically:
[
{
"dimension": { "key": "param_kv" },
"operator": "is",
"values": ["facebook"],
"paramKey": "utm_source"
}
]
Valid dimension keys are path, param_name, param_kv and param_value; param_kv requires paramKey. JSON and shorthand parameters can be mixed, and everything ANDs together.
A single request accepts at most 20 filters, 200 filter values in total across them, and 64 KB of filters JSON. Exceeding any of the three returns 400 with invalid_parameter.
Row filters
Row filters choose which parameter breakdowns are returned, without changing any click totals. As with paramKey above they are case-sensitive.
?paramKeys=redirect,utm_campaign # allowlist — only these parameters
?excludeParams=fbclid,gclid # denylist — everything except these
The allowlist is applied first, then the denylist. Path rows are always returned, so clicks, installs and fallbacks stay complete no matter which breakdowns you drop.
This is the tool for high-cardinality parameters. A link that receives a unique fbclid on every click otherwise produces one row per click; excludeParams=fbclid reduces that to the parameters you actually report on — and it is much cheaper than a click filter, because it shrinks the data before it is aggregated.
Example — Facebook traffic only, campaign parameters only, as a spreadsheet:
curl -G "https://api.godetour.link/api/analytics/stats" \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app" \
--data-urlencode "type=links" \
--data-urlencode "startDate=2026-06-01T00:00:00Z" \
--data-urlencode "endDate=2026-06-30T23:59:59Z" \
--data-urlencode "filterParam.utm_source=facebook" \
--data-urlencode "paramKeys=utm_campaign,utm_content" \
--data-urlencode "format=xlsx" \
-o june.xlsx
If a request times out with 503, narrow the date range first, then drop noisy parameters with excludeParams. Those two changes do far more than anything else.
Pagination
Pagination applies to type=links and type=events. type=overview returns one row per day for the whole range and rejects limit and offset with a 400 — it does not ignore them, so a request that sends either gets an error rather than a silently unpaginated response.
By default a response holds up to 1000 rows. Every response reports where it sits in the full result set using headers, so this works identically for json, csv and xlsx:
| Header | Meaning |
|---|---|
X-Total-Rows | Total matching rows, ignoring limit and offset |
X-Returned-Rows | Rows in this response |
X-Row-Limit | The limit applied (absent when limit=all) |
X-Row-Offset | The offset applied |
X-Truncated | true when more rows exist beyond this response |
Link | URL of the next page, as rel="next" |
Link is present only while more rows exist. X-Total-Rows is absent — and total_rows is null — when a response comes back empty at a non-zero offset, because the count travels with the rows and there are none; an empty response at offset=0 does report 0.
To read everything, follow Link: rel="next", or loop while X-Truncated is true, adding X-Returned-Rows to offset each time.
With format=json&envelope=true the same information is repeated in the body, and the rows move under a rows key:
{
"meta": {
"total_rows": 48213,
"returned_rows": 1000,
"limit": 1000,
"offset": 0,
"truncated": true,
"next_offset": 1000
},
"rows": []
}
Without envelope the body is a plain array, so existing integrations are unaffected.
Getting everything in one request
limit=all returns the complete result set instead of a page, up to 10,000 rows:
curl -X GET "https://api.godetour.link/api/analytics/stats?type=links&limit=all&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \
-H "Authorization: Bearer a1b2c3d4e5f6..." \
-H "X-App-ID: my-app"
limit is otherwise capped at 1000, and a larger number returns 400 rather than quietly giving you a short response. offset is not accepted alongside limit=all and returns 400.
Because there is no next page to point at, a limit=all response never carries a Link header, and its next_offset is null even when X-Truncated is true. To read past 10,000 rows, start again with limit/offset from offset 0.
limit=all runs as a single long query, so it is more likely to hit the timeout than paging through the same data. Prefer it for exports you run occasionally, and use limit/offset for anything on a schedule. If the result exceeds 10,000 rows the response reports X-Truncated: true — switch to limit/offset to read the remainder.
Error Handling
All errors return a JSON response with an error field.
| Status | Meaning |
|---|---|
400 | Invalid request — check your request body |
401 | Missing or invalid API key / App ID |
402 | Plan limit reached — upgrade required |
403 | Resource exists but is not accessible |
404 | Resource not found |
429 | Rate limit exceeded — back off and retry |
500 | Server error |
503 | Query timed out — narrow the request |
Support
Need help or found a bug? Join our developer community on Discord for quick support, or reach out via the Contact Form.