Skip to main content

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 TypePurposeUse Case
PublishableIdentify your app, used in SDKsClient-side SDK implementations
SecretFull platform management & administrationServer-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"
info

Secret keys must be stored securely on your server. Never expose them in client-side code or commit them to version control.

Endpoints

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 links
  • 401 Unauthorized — Invalid or missing API key
  • 429 Too Many Requests — Rate limit exceeded
  • 500 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 created
  • 400 Bad Request — Invalid request body (missing required fields)
  • 401 Unauthorized — Invalid or missing API key
  • 402 Payment Required — Short link limit reached on free plan
  • 404 Not Found — Link not found for this app
  • 429 Too Many Requests — Rate limit exceeded
  • 500 Internal Server Error — Server error

Plan Limits:

PlanShort Links Limit
Free50
PaidUnlimited

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"
}'

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 update
  • parameters (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 updated
  • 400 Bad Request — Invalid request body (missing required fields)
  • 401 Unauthorized — Invalid or missing API key
  • 404 Not Found — Short link not found
  • 429 Too Many Requests — Rate limit exceeded
  • 500 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"
}'

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 link
  • url (string) — Primary redirect URL
  • redirect_url (string) — Fallback redirect URL
  • params_pass_strategy (string) — How parameters are passed: "all", "none", or "specific"
  • specific_params (array | null) — If set, only these parameters are passed through
  • created_at (string) — Link creation timestamp (ISO 8601)
  • app_hash (string) — Hash identifier used in short link URLs

Status Codes:

  • 200 OK — Successfully retrieved link
  • 401 Unauthorized — Invalid or missing API key
  • 404 Not Found — Link not found for this app
  • 429 Too Many Requests — Rate limit exceeded
  • 500 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:

ParameterTypeRequiredDefaultDescription
typestringYesData type: "overview", "links", or "events"
startDatestringYesStart of date range (ISO 8601, e.g., 2026-06-01T00:00:00Z)
endDatestringYesEnd of date range (ISO 8601, e.g., 2026-06-30T23:59:59Z). Max range: 120 days
formatstringNojsonExport format: "json", "csv" or "xlsx"
platformstringNoallFilter by platform: "all", "ios", or "android"
linkIdstringNoUUID of a specific link to scope the query. Defaults to the main app link (relevant for "links" and "overview" types; ignored for "events")
limitstringNo1000Rows per page: an integer from 1 to 1000, or "all" for the complete set. links and events only. See Pagination
offsetnumberNo0Rows to skip. links and events only, and not accepted together with limit=all. See Pagination
envelopestringNo"true" or "1" wraps a JSON response as { meta, rows }. See Pagination

Additional parameters narrow the data itself — see Filtering.

Plan Requirements:

typeRequired Plan
overviewFree
linksPaid
eventsPaid

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_key and param_value are "") — 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 retrieved
  • 400 Bad Request — Missing or invalid type, invalid filter syntax, invalid limit/offset, or date range exceeds 120 days
  • 401 Unauthorized — Invalid or missing API key
  • 402 Payment Required — This data type requires a paid plan
  • 403 Forbidden — The requested link is not accessible with this API key
  • 429 Too Many Requests — Rate limit exceeded
  • 500 Internal Server Error — Server error
  • 503 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:

codeStatusMeaning
unauthorized401Missing or invalid API key, or unknown app
payment_required402This data type requires a paid plan
forbidden403The app or link is not accessible with this key
not_found404No active app for this X-App-ID
invalid_parameter400A parameter was malformed or out of range — the message says which
invalid_type400Unknown type
rate_limited429Rate limit exceeded — see the Retry-After header
query_timeout503The query exceeded the database time limit
export_failed500Unexpected server error while building the response
internal_error500Unexpected 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 filtersRow filters
Parametersfilters, filterPath, filterParamName, filterParamValue, filterParam.<key>paramKeys, excludeParams
DecidesWhich clicks are countedWhich breakdown rows come back
Changes click totals?YesNo
Can exclude?NoYes
Works onPaths, parameter names, parameter values, key/value pairsParameter names
info

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 matchParameterOperator parameter
PathfilterPath=/checkoutfilterPathOp
Parameter is presentfilterParamName=gclidfilterParamNameOp
Any parameter's valuefilterParamValue=summerfilterParamValueOp
A specific key = valuefilterParam.utm_source=facebookfilterParamOp
# 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
tip

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

info

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:

HeaderMeaning
X-Total-RowsTotal matching rows, ignoring limit and offset
X-Returned-RowsRows in this response
X-Row-LimitThe limit applied (absent when limit=all)
X-Row-OffsetThe offset applied
X-Truncatedtrue when more rows exist beyond this response
LinkURL 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.

caution

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.

StatusMeaning
400Invalid request — check your request body
401Missing or invalid API key / App ID
402Plan limit reached — upgrade required
403Resource exists but is not accessible
404Resource not found
429Rate limit exceeded — back off and retry
500Server error
503Query 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.