Skip to content

S2S click API ​

Report affiliate clicks server-to-server

Affiliate partners use this endpoint to report a click from their server when the user never opens a Linkrunner tracking link. For the setup guide, see Server-to-Server Clicks.

Endpoint ​

GET https://s2s.linkrunner.io/v1/click/{app_id}
GET https://s2s.linkrunner.io/v1/click?app_id={app_id}
GET https://s2s.linkrunner.io/v1/click?d={domain}

The advertiser is identified by the app ID (or the tracking link's domain) and the campaign by c. The campaign must belong to your partner account, and the advertiser must have turned on S2S clicks for you.

Authentication ​

Every request must carry your partner token. Send it in a header:

X-Linkrunner-Token: <your_token>

If your ad server can only fire URLs, send it as the lr_token query parameter instead: &lr_token=<your_token>. Linkrunner removes lr_token before it stores the click, so it never appears in reports or postbacks.

Generate the token in your affiliate dashboard under Settings → S2S Credentials. It is shown once, so store it as a secret. One token covers every app you're connected to, for both S2S clicks and impressions. A request without a valid token is rejected with 401. For rotation and revocation, see Authenticate your requests.

Parameters ​

Send all parameters as URL-encoded query parameters. Each value is limited to 256 characters.

ParameterAliasRequiredDescription
app_idpathOne of app_id or dThe app's Android package name (com.example.app) or App Store ID (id1234567890 or 1234567890). Can also be sent in the path: /v1/click/{app_id}.
dOne of app_id or dDomain of your Linkrunner tracking link, for example app.example.com. Wins over app_id when both are sent.
cYesCampaign code, the c value of your tracking link. Must match a campaign of that app or domain exactly.
lr_tokenOnly without the headerYour partner token, for ad servers that can't set the X-Linkrunner-Token header. Removed before the request is stored.
tidclickidYesYour unique click ID. Returned in the {click_id} and {tid} postback macros.
gaidadvertising_idSee belowGoogle Advertising ID, as a UUID.
idfaSee belowApple IDFA, as a UUID.
ipaf_ipSee belowThe user's device IP. Only public addresses are used.
uaaf_uaNoThe user's device user agent.
s2af_sub1NoPassthrough value, returned in {s2}.
s3af_sub2NoPassthrough value, returned in {s3}.
s4af_sub3NoPassthrough value, returned in {s4}.
click_timeNoUnix timestamp of the click, in milliseconds or seconds. Defaults to the time of the request. Returned in {click_time}.

Every click needs at least one of gaid, idfa or a public ip. A device ID matches the install exactly; an IP alone matches probabilistically.

If both a name and its alias are sent, the name wins. redirect, pid, af_siteid and af_lang are ignored, so an AppsFlyer S2S template works once the host and c are changed and your token is added.

Linkrunner never uses the IP address or user agent of the request itself, because those belong to your server.

Responses ​

The response is always JSON, never a redirect.

json
{
  "status": "accepted",
  "click_id": "42c0d258-11c3-5692-a85f-b87931249e82"
}
json
{
  "status": "rejected",
  "reason": "missing_device_id"
}
json
{
  "status": "rejected",
  "reason": "invalid_token"
}

click_id is Linkrunner's ID for the click. It is the same for every retry of the same tid on the same campaign. To match an Android install exactly without a device ID, open the Play Store with referrer=lr_ia_id%3D<click_id>.

Status codes ​

StatusReasonMeaningRetry
200Click recorded.
400missing_appNeither app_id nor d was sent.No
400missing_c, missing_tidA required parameter is missing or empty.No
400missing_device_idNo gaid, no idfa, and no public ip. An all-zero ID counts as missing.No
400invalid_device_idgaid or idfa is not a UUID, for example null or unknown.No
400unreplaced_macroA value still contains a macro, such as {click_id} or %7Bgaid%7D.No
400value_too_longA value is longer than 256 characters.No
400invalid_click_timeclick_time is not a Unix timestamp.No
400click_too_oldclick_time is more than 24 hours ago. A time in the future is treated as now.No
400unknown_appNo Linkrunner project has this app ID.No
400ambiguous_appMore than one of the advertiser's projects with S2S on for you has this app ID and campaign code (for example test and live). Send d instead.No
400unknown_domaind is not a Linkrunner click domain.No
400unknown_campaignc does not match a campaign of that app or domain.No
401missing_tokenNo X-Linkrunner-Token header and no lr_token parameter.No
401invalid_tokenThe token is wrong, revoked, or was rotated more than 24 hours ago, or your partner account is suspended.No
403partner_not_enabledThe advertiser hasn't turned on S2S clicks for the partner that owns campaign c.No
404s2s_disabledThe S2S click API is turned off.No
429rate_limitedToo many clicks for this campaign.Yes, after Retry-After
503unavailableA temporary error.Yes, after Retry-After

Examples ​

The examples read your token from the LINKRUNNER_S2S_TOKEN environment variable.

bash
curl -G "https://s2s.linkrunner.io/v1/click/com.example.app" \
  -H "X-Linkrunner-Token: $LINKRUNNER_S2S_TOKEN" \
  --data-urlencode "c=OgWmhiSXhG" \
  --data-urlencode "tid=a1b2c3d4e5" \
  --data-urlencode "gaid=38400000-8cf0-11bd-b23e-10b96e40000d" \
  --data-urlencode "ip=49.36.10.20" \
  --data-urlencode "s2=xiaomi" \
  --data-urlencode "click_time=1790242900596"
javascript
const params = new URLSearchParams({
  c: "OgWmhiSXhG",
  tid: clickId,
  gaid: advertisingId,
  ip: deviceIp,
  s2: "xiaomi",
  click_time: String(Date.now()),
});

const res = await fetch(`https://s2s.linkrunner.io/v1/click/com.example.app?${params}`, {
  headers: { "X-Linkrunner-Token": process.env.LINKRUNNER_S2S_TOKEN },
});
const body = await res.json(); // { status: "accepted", click_id: "..." }
python
import os, time, requests

res = requests.get("https://s2s.linkrunner.io/v1/click/com.example.app", params={
    "c": "OgWmhiSXhG",
    "tid": click_id,
    "gaid": advertising_id,
    "ip": device_ip,
    "s2": "xiaomi",
    "click_time": int(time.time() * 1000),
}, headers={"X-Linkrunner-Token": os.environ["LINKRUNNER_S2S_TOKEN"]}, timeout=5)
body = res.json()  # {"status": "accepted", "click_id": "..."}

An IP-only click, with no device ID:

http
GET https://s2s.linkrunner.io/v1/click/com.example.app?c=OgWmhiSXhG&tid=a1b2c3d4e6&ip=49.36.10.20&ua=Mozilla%2F5.0%20(Linux%3B%20Android%2014)
X-Linkrunner-Token: <your_token>

Retries and deduplication ​

The same tid on the same campaign always maps to the same click_id, and the click is stored once, so it is safe to retry. Retry only 429 and 503, after the delay in the Retry-After header. Keep tid unique per campaign for good: a reused tid is treated as a retry of the earlier click, so the new click is not recorded.