Skip to content

Webhooks ​

Receive real-time notifications when attribution events occur

Using an AI coding agent? Have it write your webhook receiver for you:

bash
npx @linkrunner/skills add webhooks

The skill generates a handler for your backend that verifies the linkrunner-key header and processes install/signup payloads. See Linkrunner Agent Skills.

Overview ​

Webhooks allow you to receive real-time HTTP notifications when attribution events occur in your app. When a user installs your app or signs up, Linkrunner sends a POST request to your configured endpoint with detailed attribution data.

Common use cases:

  • Server-side analytics and reporting
  • CRM integration for user onboarding
  • Real-time Slack notifications
  • Custom attribution pipelines

Configuration ​

Data Export settings with the Webhooks tab and organic user toggle

Open webhook settings

In the dashboard, go to Settings → Data Export, then select the Webhooks tab. You can also open Webhook settings directly.

Add your endpoint

Enter your public webhook URL, then click Update Webhook.

Test the endpoint

Click Test Webhook. Your endpoint must accept JSON POST requests and return a direct 2xx response.

Include organic users

Turn on Send webhooks for organic users to receive install and signup webhooks for organic users. These payloads use network_name: "ORGANIC".

Your endpoint must be publicly accessible. Linkrunner retries failed requests up to three times, but does not follow redirects.

Allowlisting Linkrunner IPs ​

If your endpoint sits behind a firewall or IP allowlist, allow inbound requests from these addresses. All Linkrunner webhooks are sent from one of them:

IP Address
8.234.94.188
8.234.80.204

These are static and will not change without advance notice. Allowlist both — webhooks may be sent from either one.

Webhook Events ​

Linkrunner sends webhooks for the following events:

EventDescription
installTriggered when an app install is attributed to a campaign or organic source
signupTriggered when a user signup event is recorded via the SDK

Install Webhook ​

The install webhook is triggered immediately when Linkrunner attributes an app installation. At this point, your app has just been opened for the first time and the user hasn't had a chance to sign up or log in yet. Because of this, user identity fields such as user_id, name, phone, email, and additional_data are only included in signup webhooks.

By default, install webhooks fire only for attributed (paid) installs. To also receive webhooks for organic users, enable Send webhooks for organic users in Settings → Webhooks. This covers both install and signup webhooks. Organic webhooks are sent with network_name set to ORGANIC, ad_channel set to null, and no campaign details.

However, the install webhook does include lr_install_id (Linkrunner's ID for the install) and device identifiers (gaid for Android, idfa for iOS) when available. You can use these to match the install with the user later when they sign up.

Use the install webhook for:

  • Tracking install counts by campaign
  • Analyzing attribution data (network, ad creative, etc.)
  • Monitoring app store conversion rates
  • Storing device IDs to link with user accounts later

Signup Webhook ​

The signup webhook is triggered when you call the .signup() method in your app via the Linkrunner SDK. This happens after the user has signed up or logged in, so the user_id field will contain the ID you passed to the SDK.

The signup webhook includes lr_install_id and device identifiers (gaid/idfa) along with the user_id, giving you a complete picture of both the device and the user. Because the install webhook carries the same lr_install_id, you can join the two payloads without relying on device identifiers. It also includes user identity fields (name, phone, email) and any custom parameters you passed via additional_data, such as referral codes or other custom key-value pairs.

Like install webhooks, signup webhooks fire only for attributed users by default. Enabling Send webhooks for organic users also delivers signup webhooks for organic users, with network_name set to ORGANIC and no campaign details.

Use the signup webhook for:

  • Linking attribution data to your user records
  • CRM integration and user onboarding flows
  • Calculating signup conversion rates from installs
  • Forwarding custom parameters (e.g., referral codes) to your backend

To receive signup webhooks, you must call .signup() in your app after the user signs up or logs in. See your SDK's usage guide for implementation details.

Payload Structure ​

All webhooks are sent as POST requests with a JSON body.

Headers ​

HeaderDescription
Content-Typeapplication/json
linkrunner-keyYour project's private key for authentication

Body Parameters ​

FieldTypeDescription
event_type"install" | "signup"The type of attribution event
lr_install_idstringLinkrunner's unique ID for the install
user_idstring | nullCustomer user ID for signup webhooks
campaign_idstringUnique identifier for the campaign
campaign_namestring | nullHuman-readable campaign name
network_namestring | nullAttribution network: ORGANIC, META, GOOGLE, etc.
ad_channelstring | nullAd channel: META, GOOGLE, TIKTOK, APPLE_SEARCH_ADS, etc.
attributed_onISO 8601 dateTimestamp when attribution occurred
installed_atISO 8601 date | nullTimestamp of app installation
store_click_atISO 8601 date | nullTimestamp of store redirect click
linkstringThe campaign link URL
app_versionstring | nullInstalled app version
gaidstring | nullGoogle Advertising ID (Android)
idfastring | nullIdentifier for Advertisers (iOS)
namestring | nullUser's name (from user_data.name passed to the SDK)
phonestring | nullUser's phone number (from user_data.phone passed to the SDK)
emailstring | nullUser's email address (from user_data.email passed to the SDK)
additional_dataobject | nullCustom parameters and device data (from the data field passed to the SDK)
meta_campaign_detailsobject | nullMeta Ads campaign details (see below)
google_campaign_detailsobject | nullGoogle Ads campaign details (see below)
apple_search_ads_detailsobject | nullApple Search Ads campaign details (see below)
campaign_detailsobject | nullGeneric ad network campaign details (see below)
device_dataobjectDevice details captured at install (see below)
location_dataobjectLocation resolved from the install's IP address (see below)

The name, phone, and email fields are populated from user_data passed to the SDK's .signup() or .trigger() methods.
The additional_data field is populated from the SDK data object.
Fields that are not available can be null or omitted from the payload.

Install ID ​

lr_install_id identifies a single install of your app. Both webhooks for that install carry the same value, so it is the join key between them: store it from the install webhook, then look it up when the signup webhook for the same install arrives.

A user who reinstalls or reuses your app on another device produces a new install, and therefore a new lr_install_id. Use user_id to group installs by user.

Meta Campaign Details ​

When the attribution is from Meta Ads, meta_campaign_details contains:

FieldTypeDescription
ad_creative_idstring | nullMeta ad creative ID
ad_creative_namestring | nullAd creative name
ad_set_idstring | nullAd set ID
ad_set_namestring | nullAd set name
campaign_group_idstring | nullCampaign group ID
campaign_group_namestring | nullCampaign group name
account_idstring | nullMeta Ads account ID
ad_objective_namestring | nullCampaign objective (e.g., APP_INSTALLS)
is_instagramboolean | nullWhether the ad was on Instagram
publisher_platformstring | nullPlatform where ad was shown
platform_positionstring | nullPlacement position

Google Campaign Details ​

When the attribution is from Google Ads, google_campaign_details contains:

FieldTypeDescription
gclidstring | nullGoogle Click ID
gbraidstring | nullGoogle app campaign tracking parameter
ga_sourcestring | nullGoogle Analytics source
ad_group_idstring | nullAd group ID
ad_group_namestring | nullAd group name

Apple Search Ads Details ​

When the attribution is from Apple Search Ads, apple_search_ads_details contains:

FieldTypeDescription
ad_group_idstring | nullApple Search Ads ad group ID
ad_group_namestring | nullAd group name
keyword_idstring | nullKeyword ID
keyword_namestring | nullKeyword name
ad_idstring | nullAd creative ID
ad_namestring | nullAd creative name
country_or_regionstring | nullCountry or region for the attribution

Campaign Details ​

For supported non-Meta and non-Google ad networks, campaign_details contains:

FieldTypeDescription
ad_network_codestring | nullInternal ad network code
ad_network_namestring | nullAd network name
campaign_idstring | nullNetwork campaign ID, when available
adset_idstring | nullNetwork ad set ID, when available
ad_creative_idstring | nullNetwork ad creative ID, when available

Device Data ​

device_data describes the device at the time of install. It is present on every install and signup webhook. The signup webhook reports the device of the install it belongs to, so both webhooks for the same lr_install_id carry the same values.

FieldTypeDescription
ip_addressstring | nullIP address the install request came from, as seen by Linkrunner's servers
user_agentstring | nullUser-Agent header of the install request. This is usually your app's HTTP client, such as okhttp/4.12.0, not a browser
manufacturerstring | nullDevice manufacturer, such as samsung
brandstring | nullDevice brand
device_namestring | nullDevice model, such as SM-S936B on Android or iPhone and iPhone 17 Pro on iOS. The user-assigned iOS device name (for example Becca's iPhone) is never sent; when that is all the SDK reported, this is null
os_versionstring | nullOperating system version
connectivitystring | nullNetwork connection at install: wifi, cellular, ethernet, offline, or unknown

Location Data ​

location_data is resolved from device_data.ip_address, so it shows where the device connected from, not its GPS position. Like device_data, it is present on every webhook and describes the install.

FieldTypeDescription
countrystring | nullCountry name, such as India
statestring | nullState or region name
citystring | nullCity name

Both objects always contain every key. A key is null when Linkrunner did not receive that value or could not resolve the IP address. The values match the device and location columns in the user files from Data Export.

Additional Data ​

The additional_data object contains custom parameters and device data passed through the SDK. This is useful for forwarding arbitrary key-value pairs like referral codes or custom identifiers through the attribution flow.

Example:

json
{
  "id": "user_123",
  "name": "Test User",
  "email": "test@example.com",
  "phone": "9876543210",
  "device_data": {},
  "referral_code": "ABC123",
  "custom_param_name": "custom_value"
}

additional_data.device_data is whatever your app passes in the SDK data object. It is separate from the top-level device_data object, which Linkrunner fills in.

Example Payloads ​

Install Event ​

json
{
  "event_type": "install",
  "lr_install_id": "e1c0f4a2-5b7d-4c19-9a3f-2d8e6b1c7a04",
  "campaign_id": "camp_XYZ123",
  "ad_channel": "META",
  "network_name": "META",
  "app_version": "2.4.1",
  "campaign_name": "Summer Promotion 2023",
  "attributed_on": "2026-03-24T08:11:00.464Z",
  "installed_at": "2026-03-24T08:11:00.464Z",
  "store_click_at": "2026-03-24T08:11:00.464Z",
  "link": "https://dl.linkrunner.io/?c=camp_XYZ123",
  "meta_campaign_details": {
    "ad_creative_id": "cr_987654321",
    "ad_creative_name": "Summer Sale Creative",
    "ad_set_id": "as_12345",
    "ad_set_name": "Mobile Users Segment",
    "campaign_group_id": null,
    "campaign_group_name": null,
    "account_id": "acc_1122334455",
    "ad_objective_name": "APP_INSTALLS",
    "is_instagram": null,
    "publisher_platform": "facebook",
    "platform_position": "feed"
  },
  "google_campaign_details": null,
  "apple_search_ads_details": null,
  "campaign_details": null,
  "gaid": "bk9384xs-p449-96ds-r132",
  "idfa": null,
  "device_data": {
    "ip_address": "203.0.113.24",
    "user_agent": "okhttp/4.12.0",
    "manufacturer": "samsung",
    "brand": "samsung",
    "device_name": "SM-S936B",
    "os_version": "14",
    "connectivity": "wifi"
  },
  "location_data": {
    "country": "India",
    "state": "Karnataka",
    "city": "Bengaluru"
  }
}

Install Event (Organic) ​

Sent only when Send webhooks for organic users is enabled:

json
{
  "event_type": "install",
  "lr_install_id": "7f3b9d61-24ae-4f08-b512-c9a70e3d8b45",
  "campaign_id": "",
  "ad_channel": null,
  "network_name": "ORGANIC",
  "app_version": "2.4.1",
  "campaign_name": null,
  "attributed_on": "2026-03-24T08:11:00.464Z",
  "installed_at": "2026-03-24T08:11:00.464Z",
  "store_click_at": null,
  "link": null,
  "meta_campaign_details": null,
  "google_campaign_details": null,
  "apple_search_ads_details": null,
  "campaign_details": null,
  "gaid": "bk9384xs-p449-96ds-r132",
  "idfa": null,
  "device_data": {
    "ip_address": "198.51.100.7",
    "user_agent": "okhttp/4.12.0",
    "manufacturer": "Xiaomi",
    "brand": "Redmi",
    "device_name": "23129RN51X",
    "os_version": "13",
    "connectivity": "cellular"
  },
  "location_data": {
    "country": "India",
    "state": "Maharashtra",
    "city": "Pune"
  }
}

Signup Event ​

json
{
  "event_type": "signup",
  "lr_install_id": "a48d2c07-6e91-4b33-8f5a-1b0c9de74265",
  "user_id": "test_user_webhook_002",
  "campaign_id": "OgWmhiSXhG",
  "ad_channel": "META",
  "network_name": "META",
  "app_version": "1.0.0",
  "campaign_name": "TOF - Free trial - AAA - DSDT - 17/12",
  "attributed_on": "2026-03-25T15:52:00.007Z",
  "installed_at": "2025-12-30T12:34:29.742Z",
  "store_click_at": null,
  "link": "https://dl.linkrunner.io/?c=OgWmhiSXhG&utm_source=meta_ads",
  "meta_campaign_details": null,
  "google_campaign_details": null,
  "apple_search_ads_details": null,
  "campaign_details": null,
  "gaid": "5faa2433-d7e1-4a8e-9a1c-a5880c26ab5c",
  "idfa": null,
  "name": "Test User",
  "phone": "9876543210",
  "email": "test@example.com",
  "additional_data": {
    "id": "test_user_webhook_002",
    "name": "Test User",
    "email": "test@example.com",
    "phone": "9876543210",
    "device_data": {},
    "referral_code": "ABC123",
    "custom_param_name": "custom_value"
  },
  "device_data": {
    "ip_address": "203.0.113.58",
    "user_agent": "Dart/3.4 (dart:io)",
    "manufacturer": "Google",
    "brand": "google",
    "device_name": "Pixel 8",
    "os_version": "15",
    "connectivity": "wifi"
  },
  "location_data": {
    "country": "India",
    "state": "Delhi",
    "city": "New Delhi"
  }
}

Authentication ​

Every webhook request includes a linkrunner-key header containing your project's private key. Use this to verify that requests are genuinely from Linkrunner.

javascript
// Example: Verifying the webhook key
const PRIVATE_KEY = process.env.LINKRUNNER_PRIVATE_KEY;

app.post('/webhook', (req, res) => {
  const receivedKey = req.headers['linkrunner-key'];

  if (receivedKey !== PRIVATE_KEY) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Process the webhook...
  res.status(200).json({ received: true });
});

Never expose your private key in client-side code. Store it securely as an environment variable.

Slack Integration ​

Linkrunner automatically formats webhook payloads for Slack when your URL contains hooks.slack.com. Instead of raw JSON, Slack receives a rich Block Kit formatted message displaying:

  • Event type and user ID
  • App version and network
  • Campaign name
  • Attribution timestamps
  • Device identifiers (GAID/IDFA)
  • User identity (name, phone, email)
  • Meta, Google, Apple Search Ads, or generic campaign details, when available

To set up Slack notifications:

  1. Create an Incoming Webhook in your Slack workspace
  2. Copy the webhook URL (format: https://hooks.slack.com/services/...)
  3. Paste it as your webhook URL in Linkrunner settings

Retry Behavior ​

If your endpoint fails to respond with a 2xx status code, Linkrunner makes up to 3 attempts total with exponential backoff:

AttemptDelay
1st attemptImmediate
2nd attempt1 second after the first failure
3rd attempt2 seconds after the second failure

After 3 failed attempts, the webhook is marked as failed. Ensure your endpoint is reliable to avoid missing events.

Best Practices ​

Respond quickly

Return a 2xx status code as fast as possible. Process the webhook data asynchronously to avoid timeouts.

Implement idempotency

Store processed webhook events using event_type and lr_install_id, plus user_id when present.

Validate authentication

Always verify the linkrunner-key header matches your private key before processing.

Handle failures gracefully

Log failed webhook processing for debugging and implement alerting for critical failures.

Troubleshooting ​

Webhooks not being received?

  • Verify your endpoint URL is correct and publicly accessible
  • Check that your server accepts POST requests with JSON body
  • Ensure your firewall allows incoming requests from Linkrunner

Getting 401 errors?

  • Verify the linkrunner-key header validation in your code
  • Check your private key matches the one in your dashboard settings

Missing data in payload?

  • name, phone, and email require you to pass user_data to the SDK's .signup() or .trigger() methods
  • user_id requires SDK configuration to be passed
  • Device identifiers (gaid/idfa) depend on user consent and SDK implementation
  • additional_data requires passing a data object to the SDK and contains those custom parameters
  • Network-specific detail objects are only populated when that network provided attribution data

Need help? Contact support@linkrunner.io