Appearance
Server-to-Server Clicks
Report clicks from your server when the user never opens a tracking link
When to use S2S clicks
Normally the user taps your ad, opens the Linkrunner tracking link in a browser, and is redirected to the store. Linkrunner records the click on the way through.
Some placements never open a browser. OEM app-store icons on Xiaomi, OPPO and vivo, preloaded placements, and some in-app units send the user straight to the Play Store or App Store. Linkrunner never sees those clicks, so their installs look organic.
For this traffic, your server reports each click to Linkrunner, and the user goes directly to the store as before. It works like server-to-server clicks in AppsFlyer (redirect=false), and accepts the same parameter names.
Every S2S request must carry your partner token. See Authenticate your requests.
The advertiser turns on S2S clicks for each partner separately. Ask them, or Linkrunner support, to turn them on for your partner account before you send traffic. Until then, requests are rejected with partner_not_enabled. S2S clicks stop if the advertiser disconnects your account.
How it works
User taps your ad ──────────────────────────► Play Store / App Store
│ │
│ your server, in real time │ user installs and opens the app
▼ ▼
GET s2s.linkrunner.io/v1/click/<app_id> Linkrunner SDK reports the install
│ │
└──────────── Linkrunner matches the install to your click
│
▼
Install postback to your endpoint
({click_id}, {tid}, {s2}..{s4}, {click_time}, ...)Linkrunner matches the install to your click in one of three ways, strongest first:
| Match | What you send | Precision |
|---|---|---|
| Referrer | Open the Play Store with referrer=lr_ia_id%3D<click_id>, using the click_id from our response | Exact |
| Device ID | gaid (Android) or idfa (iOS) on the click | Exact |
| IP | The user's public device ip, plus ua, when there is no device ID | Probabilistic |
Send a device ID whenever you have one. IP matching is a fallback: users behind the same public IP can be confused with each other.
Only report ads the user tapped. For ads that were only shown, use S2S impressions.
Postbacks work exactly as for regular clicks. Your existing postback templates, event mapping and postback logs apply unchanged.
Set it up
Get your S2S token
In your affiliate dashboard, open Settings → S2S Credentials and click Generate token. Copy the token (it starts with lrs2s_) and store it as a secret on your server. It is shown only once.
One token works for every app you're connected to, so you only do this once.
Create the campaign link
Create the link in your affiliate dashboard as usual. See Create a campaign link. You get a tracking link like this:
https://app.example.com/?c=OgWmhiSXhG&tid={click_id}&s2={campaign_id}Turn it into the S2S URL
Keep your parameters, and send them to https://s2s.linkrunner.io/v1/click/<app_id> instead. <app_id> is the app's Android package name (com.example.app) or its App Store ID (id1234567890), the same app ID you use with AppsFlyer.
Then add the device fields your server has: gaid or idfa, and optionally ip, ua and click_time. Your token goes in the X-Linkrunner-Token header:
http
GET https://s2s.linkrunner.io/v1/click/com.example.app?c=OgWmhiSXhG&tid={click_id}&s2={campaign_id}&gaid={gaid}&ip={device_ip}&ua={user_agent}&click_time={timestamp_ms}
X-Linkrunner-Token: <your_token>You can name the advertiser by the tracking link's domain instead: https://s2s.linkrunner.io/v1/click?d=app.example.com&c=.... Use d if the advertiser tells you their test and live apps share an app ID and the same campaign code.
Fire it from your server when the user clicks
Replace your macros and call the URL with GET and the token header, at the moment of the click. The user doesn't wait on this call; send them to the store directly.
A 200 with "status":"accepted" means the click is recorded. See retries for the other responses.
Test it end to end
Send one click with the GAID of a test device, then install and open the app on that device. The install postback arrives with your tid in {click_id} / {tid}. Check it in Postback logs.
Send each click as it happens. Linkrunner only attributes an install to a click it received first, and there is no way to re-attribute an install afterwards. Clicks older than 24 hours are rejected.
Authenticate your requests
Linkrunner rejects any S2S request that doesn't carry your partner token, with 401 and missing_token or invalid_token. The token proves the traffic comes from you, so nobody else can send clicks in your name.
Send it in the X-Linkrunner-Token header on every request. This example reads it 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"If your ad server can only fire a URL and can't set headers, add the token as the lr_token query parameter instead:
text
https://s2s.linkrunner.io/v1/click/com.example.app?lr_token=<your_token>&c=OgWmhiSXhG&tid={click_id}&gaid={gaid}Linkrunner removes lr_token before it stores the click, so the token never appears in reports or postbacks. Use the header when you can, because full URLs often end up in server logs.
Your token is valid for every app you're connected to, for both S2S clicks and S2S impressions.
Rotate or revoke your token
Manage your token in Settings → S2S Credentials.
- Rotate: generate a new token. Your previous token keeps working for 24 hours, so update your servers within that time. If you rotate again within those 24 hours, your older token keeps its original expiry, and the token from your last rotation stops working right away.
- Revoke all: stops every token at once. All your S2S requests are rejected until you generate a new token and send it.
New, rotated and revoked tokens take up to a minute to apply.
Treat the token like a password. Don't put it in client apps, public repositories or tracking links you share. If it leaks, use Revoke all and then generate a new token. Rotating alone keeps the leaked token working for 24 hours.
Coming from AppsFlyer
If you already send S2S clicks to AppsFlyer, you can reuse that template. Linkrunner takes the app ID in the path just like AppsFlyer and accepts the AppsFlyer parameter names. You change the host and c, and add your token.
| AppsFlyer | Linkrunner | Notes |
|---|---|---|
app.appsflyer.com/<app_id> | s2s.linkrunner.io/v1/click/<app_id> | The same app ID: Android package name or App Store ID |
c (campaign name) | c (campaign code) | Use the c value from your Linkrunner tracking link, not a campaign name |
clickid | tid | Either name works |
advertising_id | gaid | Either name works |
idfa | idfa | Same |
af_ip | ip | Either name works |
af_ua | ua | Either name works |
af_sub1, af_sub2, af_sub3 | s2, s3, s4 | Either name works. Returned in {s2}–{s4} |
redirect=false | Not needed | This endpoint never redirects. Ignored if present |
pid, af_siteid, af_lang | Not needed | Ignored if present |
An AppsFlyer template like this:
https://app.appsflyer.com/com.example.app?pid=xyz_int&c=summer&clickid={click_id}&advertising_id={gaid}&af_ip={ip}&af_ua={ua}&af_sub1={pub}&redirect=falsebecomes:
http
GET https://s2s.linkrunner.io/v1/click/com.example.app?c=OgWmhiSXhG&clickid={click_id}&advertising_id={gaid}&af_ip={ip}&af_ua={ua}&af_sub1={pub}
X-Linkrunner-Token: <your_token>Retries
Retrying is safe. The same tid for the same campaign always returns the same click_id and is stored once.
- Retry
429and503, after theRetry-Afterdelay. - Don't retry other
4xxresponses. Fix the request instead. Thereasontells you what's wrong.
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.
See the S2S Click API reference for every parameter, response and rejection reason.
Troubleshooting
Requests return missing_token. The request has no X-Linkrunner-Token header and no lr_token parameter. Check that your server or ad platform sends the header on every call, not only on some of them.
Requests return invalid_token. The token is wrong, was revoked, or was replaced more than 24 hours ago. You also get this while your partner account is suspended. Copy the current token from your server's secret store, not from an old template. If you no longer have it, rotate it in Settings → S2S Credentials.
Every request returns unknown_campaign.c must be the campaign code from your Linkrunner tracking link. An AppsFlyer campaign name in c doesn't match.
Requests return partner_not_enabled. The advertiser hasn't turned on S2S clicks for your partner account on this app, or c is a campaign that isn't yours. Changes take up to 5 minutes to apply.
Requests return unknown_app. Check the app ID against the store listing: the package name for Android, id plus the number for iOS. An app newly set up in Linkrunner can take a few minutes to be recognised.
Requests return ambiguous_app. The advertiser has more than one Linkrunner project for this app (for example test and live), and the campaign code exists in both. Send d with the tracking link's domain instead of the app ID.
Requests return unreplaced_macro. A value still contains a macro such as {click_id} or %7Bgaid%7D. Your platform must replace every macro before it sends the request.
Clicks are accepted but installs aren't attributed. Check that the click carries the user's real GAID or IDFA, not a placeholder, and that it arrives before the install. With IP only, make sure ip is the user's device IP, not your server's. A device that clicked another source more recently may be credited to that click instead.
Postbacks arrive without your click ID. Confirm the click had tid (or clickid) and the response was accepted.
Need help? Contact support@linkrunner.io