Home Guides

Guides

How-to guides for LinkMe
By Tomas Radvansky
5 articles

Webhooks Guide

Use LinkMe webhooks when you want near real-time event delivery to your own systems or third-party tools. You can subscribe to: - link.click - link.token_created - link.claim - link.app_open - link.deferred_claim_attempt What this is for Webhooks let you: - stream LinkMe events into your backend - trigger automation (CRM, notifications, workflows) - forward events into analytics pipelines (PostHog, GA4, Segment, warehouse) - keep a delivery audit trail without polling APIs Configure a webhook in Portal 1. Go to Portal -> App -> Developer -> Webhooks. 2. Enter your webhook URL (https://...). 3. Select one or more event types. 4. Optionally enable signing secret (auto-generated by LinkMe). 5. Save. Each webhook can be enabled/disabled independently. Payload shape Each request body follows this envelope: { "id": "uuid", "event": "link.click", "ts": "2026-02-11T12:34:56.789Z", "app_id": "app_123", "data": { "link_id": "abc123", "platform": "ios", "dest_url": "https://example.com" } } data fields vary by event type. Delivery behavior LinkMe delivery behavior includes: - automatic retries for retriable errors (for example 5xx, timeouts) - backoff between retry attempts - final stop after max attempts - per-attempt delivery history with status and response snippet - auto-disable if failures keep repeating You can inspect recent deliveries in Developer -> Webhooks -> Deliveries. Optional request signing If signing is enabled, LinkMe auto-generates a secret for the webhook and includes: - X-LinkMe-Signature: sha256=<hmac> The signature is HMAC-SHA256 over the raw request body using your secret. Node.js verification example import crypto from 'node:crypto'; export function verifyLinkMeSignature(rawBody: string, secret: string, signatureHeader: string): boolean { if (!signatureHeader || !signatureHeader.startsWith('sha256=')) return false; const receivedHex = signatureHeader.slice('sha256='.length); const expectedHex = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const received = Buffer.from(receivedHex, 'hex'); const expected = Buffer.from(expectedHex, 'hex'); if (received.length !== expected.length) return false; return crypto.timingSafeEqual(received, expected); } Node SDK shortcut (recommended) If you use @li-nk.me/node-sdk, use built-in helpers instead of maintaining custom verification code: import { parseLinkMeWebhookEnvelope, verifyLinkMeWebhookSignature, } from '@li-nk.me/node-sdk'; const rawBody = req.rawBody.toString('utf8'); const signature = req.get('X-LinkMe-Signature'); if (!verifyLinkMeWebhookSignature(rawBody, signature, process.env.LINKME_WEBHOOK_SIGNING_SECRET!)) { return res.status(401).json({ ok: false, error: 'Invalid signature' }); } const envelope = parseLinkMeWebhookEnvelope(JSON.parse(rawBody)); // forward `envelope` to your own pipeline (queue, analytics provider, data warehouse, etc.) return res.status(200).json({ ok: true }); Python verification example import hmac import hashlib def verify_linkme_signature(raw_body: bytes, secret: str, signature_header: str) -> bool: if not signature_header or not signature_header.startswith("sha256="): return False received_hex = signature_header[len("sha256="):] expected_hex = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(received_hex, expected_hex) Common integration pattern Typical production setup: 1. Receive LinkMe webhook at your backend endpoint. 2. Verify signature (if enabled). 3. Validate and parse event + data. 4. Push to your destination (PostHog, GA4, queue, warehouse). 5. Return 2xx quickly. Keep heavy downstream processing async in your worker/queue layer. GA4 server-side setup (Measurement Protocol) This is the setup used when LinkMe webhook events are forwarded from your backend to GA4 as server-side events. 1) Create/get the GA4 Measurement ID In GA4, open your property and select a data stream. Copy the stream's Measurement ID (format: G-XXXXXXXXXX). Store this as: - GA4_MEASUREMENT_ID (regular environment variable) 2) Create a GA4 API secret For the same data stream, open Measurement Protocol API secrets and create a new secret. Store this as: - GA4_API_SECRET (secret, not plain env) 3) Configure LinkMe webhook signature secret If webhook signing is enabled in LinkMe, copy the signing secret and store it as: - LINKME_WEBHOOK_SIGNING_SECRET (secret) Your receiver should verify: - X-LinkMe-Signature: sha256=<hmac> against the raw request body. 4) Deploy backend config For a Firebase Functions backend, a common setup is: - keep GA4_MEASUREMENT_ID in function env/config - load GA4_API_SECRET from Secret Manager - load LINKME_WEBHOOK_SIGNING_SECRET from Secret Manager - ensure runtime service account has roles/secretmanager.secretAccessor Example (Google Cloud CLI): gcloud secrets create GA4_API_SECRET --replication-policy="automatic" printf '%s' 'YOUR_GA4_API_SECRET' | gcloud secrets versions add GA4_API_SECRET --data-file=- gcloud secrets create LINKME_WEBHOOK_SIGNING_SECRET --replication-policy="automatic" printf '%s' 'YOUR_LINKME_SIGNING_SECRET' | gcloud secrets versions add LINKME_WEBHOOK_SIGNING_SECRET --data-file=- 5) Configure LinkMe webhook endpoint In LinkMe Portal: 1. Add your HTTPS endpoint (example: /webhooks/linkme). 2. Subscribe to the events you need (link.click, link.claim, etc.). 3. Enable signing for production. 6) Make the data meaningful in GA4 Forwarding events is not enough by itself. To make reports usable: - keep stable event names (for example linkme_link_click, linkme_link_claim) - include stable params such as linkme_event, linkme_event_id, linkme_app_id, linkme_link_id, linkme_platform - register important params as Custom dimensions in GA4 - mark business-critical events (for example claim/activation) as Key events - optionally enable BigQuery export for deeper attribution analysis Quick verification checklist - webhook delivery in LinkMe shows 2xx - backend logs show successful GA4 Measurement Protocol POST - event appears in GA4 Realtime - custom dimensions start populating in standard reports (can take time) Related - Analytics Integration Guide - Deferred Deep Linking Guide - Developer Setup

Last updated on Apr 04, 2026

Chrome Extension Guide

Use the li-nk.me Chrome extension when you want to create short links from the page you are already viewing instead of switching back to the portal. What it is for The extension is useful when you need to: - shorten the current tab in one click - paste a destination URL and create a short link quickly - add a custom slug for a campaign, content post, or support reply - keep new links inside the same li-nk.me app you already manage in Portal Install the extension Install the official extension from the Chrome Web Store. Before you start You need: - a li-nk.me account - an app in Portal - a write-capable API key for that app If you have not created those yet, start in Portal. Configure the extension 1. Install the extension and pin it in Chrome if you want faster access. 2. Open the li-nk.me popup. 3. Open Settings. 4. Enter your App ID. 5. Enter your API key. 6. Save the configuration. The extension stores these values locally in your browser so it can create links for the selected app. Create a short link 1. Open the page you want to share. 2. Click the li-nk.me extension icon. 3. Choose Use current tab or paste a destination URL manually. 4. Optionally enter a custom slug. 5. Click Create short link. 6. Copy the generated URL or open it immediately. How it works with Portal Links created from the extension are associated with the app configured in settings. That means you can still: - review them in Portal - inspect click analytics - manage other app settings and API keys centrally Recommended usage The extension is a good fit for: - growth and lifecycle teams creating links during campaign QA - content teams shortening live landing pages or blog posts - customer success and support teams sharing clean URLs quickly - internal testing when you want a short link without leaving Chrome Related - Chrome extension landing page - Short Link Builder - Developer auth and API keys

Last updated on Apr 04, 2026

Passthrough Universal Links

LinkMe supports two types of link behavior on your branded domains: stored links with payloads and passthrough universal links. Both open your app via Universal Links (iOS) or App Links (Android), but the payload and tracking are different. Stored links (deferred links) Stored links are created in the Portal and mapped to a specific slug (for example, /promo-spring). When someone taps the link: - The link resolves to a stored record in LinkMe. - The SDK receives a full payload: linkId, path, utm, custom, and any configured deep link data. - Click tokens and deferred-claim flows are recorded for install attribution. - Analytics and link metadata (UTM presets, custom data, overrides) are applied. This is the right choice when you need analytics, attribution, or custom payloads that must be managed per link. Passthrough universal links Passthrough links are unknown slugs on your branded domain. LinkMe treats the full path and query as the app route without requiring a stored link. Example: https://link.example.com/promo/summer/2025?code=VIP&utm_source=paid When passthrough is enabled: - The app opens as a normal Universal Link/App Link. - The SDK resolves the link via /api/deeplink/resolve-url and receives a lightweight payload: - path: /promo/summer/2025 - params: query parameters excluding UTM keys - utm: UTM keys detected in the query - No linkId or cid is created. - No per-link analytics or custom payloads are attached. This is the right choice when you just want raw routing into the app without provisioning a stored link first. Non-LinkMe universal links If your app receives a universal link for a domain that is not connected to LinkMe, the SDK still emits a payload so you can react to it: - isLinkMe will be false. - url contains the original universal link. - path, params, and utm are parsed from the URL (UTM keys are split out). This gives you a consistent payload shape even when the link is a basic universal link rather than a LinkMe-managed domain. Toggling passthrough behavior In App Settings, the Disable path passthrough for unknown slugs toggle controls this feature: - Off (default): unknown slugs are treated as passthrough universal links. - On: unknown slugs do not pass through to the app and fall back to standard web behavior. Choosing between them Use stored links when you need payloads, attribution, or analytics. Use passthrough links when you want the domain to behave like a pure universal link host without creating link records for every path.

Last updated on Apr 04, 2026

Analytics Integration Guide

LinkMe resolves deep links and passes attribution data (UTM parameters) directly to your app. To view this data in your analytics dashboard (Firebase, PostHog, Mixpanel, etc.), you must manually log the campaign event when a link is received. [!IMPORTANT] Why is manual logging required? While some analytics SDKs attempt to automatically scrape "direct" deep links from the OS, they cannot detect Deferred Deep Links (links that survive the app install process). LinkMe fetches deferred data via an API call after your app launches. To attribute these installs correctly, you must take the payload from LinkMe and hand it to your analytics provider. Prefer server-side streaming? If you want LinkMe to push events directly to your backend (instead of only SDK-side logging), use the dedicated Webhooks Guide. It covers event subscriptions, delivery history, retries/backoff, and optional request signing. When to Log You should log the campaign event in two places to cover all scenarios: 1. Cold Start / Install: Inside the getInitialLink callback. 2. Warm Start: Inside the addListener (or onLink) callback. Link analytics dashboard showing click totals, UTM properties, and payload details The portal logs mirror what your downstream analytics stack should store—UTM keys, link IDs, and the computed destination—so you can sanity-check payloads before emitting events. If you are still validating routing before wiring analytics, run the Universal Links Validator, review link analytics, or open the Portal to inspect real click payloads and campaign metadata side by side. Firebase Analytics (Google Analytics 4) Official Documentation: Log events Firebase uses specific parameter names (source, medium, campaign) instead of standard UTM keys (utm_source, etc.). You must map them. Android (Kotlin) import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics.Event import com.google.firebase.analytics.FirebaseAnalytics.Param fun logToFirebase(payload: LinkPayload) { val utm = payload.utm ?: return val bundle = Bundle().apply { // Standard Firebase Campaign Params putString(Param.SOURCE, utm["utm_source"]) putString(Param.MEDIUM, utm["utm_medium"]) putString(Param.CAMPAIGN, utm["utm_campaign"]) putString(Param.TERM, utm["utm_term"]) putString(Param.CONTENT, utm["utm_content"]) // Add the LinkMe ID for reference putString("link_id", payload.linkId) } // Log the standard CAMPAIGN_DETAILS event FirebaseAnalytics.getInstance(context).logEvent(Event.CAMPAIGN_DETAILS, bundle) } iOS (Swift) import FirebaseAnalytics func logToFirebase(payload: LinkPayload) { guard let utm = payload.utm else { return } var params: [String: Any] = [:] // Map UTM keys to Firebase constants if let source = utm["utm_source"] { params[AnalyticsParameterSource] = source } if let medium = utm["utm_medium"] { params[AnalyticsParameterMedium] = medium } if let campaign = utm["utm_campaign"] { params[AnalyticsParameterCampaign] = campaign } if let term = utm["utm_term"] { params[AnalyticsParameterTerm] = term } if let content = utm["utm_content"] { params[AnalyticsParameterContent] = content } if let linkId = payload.linkId { params["link_id"] = linkId } // Log the standard campaign_details event Analytics.logEvent(AnalyticsEventCampaignDetails, parameters: params) } PostHog Official Documentation: Capture events PostHog accepts standard UTM parameters directly. You can pass them as properties in a Deep Link Opened event or identify the user with them. // JavaScript / React Native example posthog.capture('Deep Link Opened', { $current_url: payload.path, utm_source: payload.utm?.utm_source, utm_medium: payload.utm?.utm_medium, utm_campaign: payload.utm?.utm_campaign, link_id: payload.linkId }); Segment Official Documentation: Track Segment recommends passing campaign data in the context.campaign object. analytics.track('Deep Link Opened', { linkId: payload.linkId, path: payload.path }, { campaign: { name: payload.utm?.utm_campaign, source: payload.utm?.utm_source, medium: payload.utm?.utm_medium, term: payload.utm?.utm_term, content: payload.utm?.utm_content } }); Amplitude Official Documentation: User Properties Amplitude recommends tracking UTM parameters as User Properties (for attribution) or Event Properties (for specific campaign interactions). // Identify user with UTM properties (Attribution) amplitude.identify(new amplitude.Identify() .set('utm_source', payload.utm?.utm_source) .set('utm_medium', payload.utm?.utm_medium) .set('utm_campaign', payload.utm?.utm_campaign) ); // Track the open event amplitude.track('Deep Link Opened', { link_id: payload.linkId, ...payload.utm // Spread all UTM params as event properties }); Summary Checklist - [ ] Map Parameters: Ensure you map utm_source to source if your provider requires it (like Firebase). - [ ] Log on Install: Ensure getInitialLink triggers the log event so you capture ad-driven installs. - [ ] Log on Open: Ensure addListener triggers the log event for re-engagement campaigns. - [ ] Validate the link first: Confirm your AASA or assetlinks.json responses before debugging analytics payloads. Need the managed side as well? See pricing for plan details, custom domain setup for branded links, and the Webhooks Guide if you want LinkMe to stream events directly to your backend.

Last updated on Apr 04, 2026

Deferred Deep Linking Guide

import MermaidChart from '@site/src/components/MermaidChart'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Deferred Deep Linking Deferred deep linking allows you to pass link data through the app store installation process. When a user clicks a link but doesn't have your app installed, li-nk.me preserves the link data so the app can retrieve it after installation. If you are setting this up for the first time, pair this guide with the Universal Links Validator, custom domain setup, and the Portal so you can verify the association files and app settings before testing install flows. How It Works The Problem When a user clicks a link: 1. App is installed → Universal Link / App Link opens the app directly with the full payload 2. App is NOT installed → User is redirected to the App Store / Play Store In case #2, the original link context is normally lost. Deferred deep linking solves this. The Solution li-nk.me uses platform-specific mechanisms to recover the original link data: | Platform | Primary Method | Fallback | |----------|----------------|----------| | Android | Play Install Referrer API | Fingerprint matching | | iOS (Native/Flutter) | Pasteboard (if enabled in Portal) | Fingerprint matching | | iOS (React Native) | Pasteboard (with expo-clipboard) | Fingerprint matching | :::note SDK Support - Native iOS SDK & Flutter: Full pasteboard support (automatic) - React Native: Install expo-clipboard to enable pasteboard support, otherwise uses fingerprint matching ::: Architecture <MermaidChart chart={` flowchart TB click["User clicks https://link.app/promo"] edge["li-nk.me Edge Server1. Create click_token (cid)2. Hash IP + UA for fingerprint3. Redirect to App/Play Store"] click --> edge subgraph Android storeA["Play Store redirect<br/>referrer=cid=xxx"] installA["User installs app"] launchA["First launch<br/>1. SDK reads Install Referrer<br/>2. If cid: GET /api/deeplink?cid=xxx<br/>3. Else: POST /api/deferred/claim"] end subgraph iOS storeI["App Store redirect<br/>Pasteboard write if enabled"] installI["User installs app"] launchI["First launch<br/>1. SDK reads pasteboard (if enabled)<br/>2. If cid missing: POST /api/deferred/claim"] end edge --> storeA edge --> storeI storeA --> installA --> launchA storeI --> installI --> launchI payload["Payload returned to app<br/>{ linkId, path, params, utm, custom }"] launchA --> payload launchI --> payload `} /> Implementation Android uses the Play Install Referrer API as the primary mechanism. When li-nk.me redirects to the Play Store, it embeds the click token (cid) in the referrer parameter: https://play.google.com/store/apps/details?id=com.app&referrer=cid%3Dxxx After installation, the SDK reads this referrer and claims the payload directly. // In your Application or first Activity LinkMe.shared.configure(context, config) // Check for initial link from direct open LinkMe.shared.getInitialLink { payload -> if (payload != null) { // App opened via direct link routeUser(payload) } else { // No direct link — check for deferred (first install) LinkMe.shared.claimDeferredIfAvailable(context) { deferred -> if (deferred != null) { // First install from a link! routeUser(deferred) } } } } Dependencies required: implementation("com.android.installreferrer:installreferrer:2.2") iOS uses pasteboard (when enabled in Portal) or fingerprint matching for deferred deep linking. When pasteboard is enabled in the Portal (App Settings → iOS → "Enable Pasteboard for Deferred Links"): 1. The redirect page writes a URL containing the cid to the system pasteboard 2. After installation, the SDK checks the pasteboard for a li-nk.me URL 3. If found, it claims the payload directly via the cid 4. If not found, falls back to fingerprint matching App settings screen with the deferred deep linking toggle enabled Toggle pasteboard support inside the App Settings view before shipping so new installs can recover their click tokens without relying solely on fingerprint matching. // In your App init LinkMe.shared.configure(config: .init( baseUrl: baseUrl, appId: appId, appKey: appKey )) // Check for initial link from direct Universal Link open LinkMe.shared.getInitialLink { payload in if let p = payload { // App opened via direct Universal Link routeUser(p) } else { // No direct link — check for deferred (first install) // SDK automatically checks pasteboard first (if enabled), then fingerprint LinkMe.shared.claimDeferredIfAvailable { deferred in if let d = deferred { // First install from a link! routeUser(d) } } } } # Required npm install @li-nk.me/react-native-sdk # Optional but recommended: enables pasteboard support on iOS npx expo install expo-clipboard Package: @li-nk.me/react-native-sdk import { configure, getInitialLink, claimDeferredIfAvailable, } from '@li-nk.me/react-native-sdk'; await configure({ baseUrl, appId, appKey }); const initial = await getInitialLink(); if (initial?.path) { routeUser(initial); } else { // No direct link — check for deferred (first install) // On iOS: checks pasteboard first (if expo-clipboard installed), then fingerprint // On Android: uses fingerprint matching (Install Referrer not available in JS) const deferred = await claimDeferredIfAvailable(); if (deferred?.path) { routeUser(deferred); } } :::tip iOS Pasteboard When expo-clipboard is installed, the SDK automatically checks the iOS pasteboard for a li-nk.me URL before falling back to fingerprint matching. This requires pasteboard to be enabled in the Portal (App Settings → iOS). ::: final linkme = LinkMe(); await linkme.configure(config); final initial = await linkme.getInitialLink(); if (initial != null) { routeUser(initial); } else { // No direct link — check for deferred (first install) final deferred = await linkme.claimDeferredIfAvailable(); if (deferred != null) { routeUser(deferred); } } Best Practices 1. Always Check Both The recommended pattern is: 1. Check getInitialLink() first (handles direct opens) 2. If null, call claimDeferredIfAvailable() (handles first installs) This ensures you catch both scenarios. 2. Call Only on First Launch Deferred claim should only be called on the first app launch after installation. Subsequent launches should skip it: val prefs = getSharedPreferences("linkme", MODE_PRIVATE) if (!prefs.getBoolean("deferred_checked", false)) { prefs.edit().putBoolean("deferred_checked", true).apply() LinkMe.shared.claimDeferredIfAvailable(context) { /* ... */ } } 3. Handle Duplicates The API returns a duplicate flag when the same fingerprint has already claimed a link. Use this to avoid re-routing users who have already been attributed. 4. Time Window Fingerprint matches are valid for 72 hours by default. Links clicked more than 72 hours before installation cannot be matched via fingerprinting. Fingerprint Matching Accuracy Fingerprint matching uses IP address + User-Agent hash. While not 100% accurate, it provides a good balance: | Scenario | Accuracy | |----------|----------| | Same network, same device | High | | Same network, different device | False positive possible | | Different network (VPN, carrier change) | Will not match | | Shared public IP (office, school) | False positive possible | Note: Android Install Referrer is deterministic and does not have these limitations. Pasteboard Flow (iOS) Pasteboard is controlled from the Portal (not SDK config): 1. Enable in Portal: App Settings → iOS → "Enable Pasteboard for Deferred Links" 2. When enabled, the redirect page writes a URL containing the cid to the system pasteboard 3. After installation, the SDK automatically checks the pasteboard for a li-nk.me URL 4. If found, it extracts the cid and claims the payload directly (more reliable than fingerprint) 5. If not found, falls back to fingerprint matching Considerations: - iOS 14+ prompts users when an app reads the pasteboard ("app has pasted from...") - This prompt appears the first time the app reads the pasteboard - The SDK only reads pasteboard URLs that match the li-nk.me domain format - Enable this if fingerprint matching accuracy is insufficient for your use case Claim Types The claim response includes a claim_type field indicating how the link was matched: | Type | Description | |------|-------------| | direct | Claimed via cid from Install Referrer or pasteboard | | install_referrer | Claimed via Android Play Install Referrer | | probabilistic | Claimed via fingerprint matching | | pasteboard | Claimed via iOS pasteboard read | Troubleshooting Android: Install Referrer Not Working 1. Ensure com.android.installreferrer:installreferrer dependency is added 2. Test with an actual Play Store install (sideloading won't work) 3. Check if the referrer parameter is being passed in the Play Store URL iOS: Fingerprint Not Matching 1. Verify the app launch happens on the same network as the click 2. Check if the click is within the 72-hour window 3. User-Agent must match (same browser/device) General: No Payload Returned 1. Verify the link was created in li-nk.me (not just a random URL) 2. Check that the link's app has valid configuration 3. Ensure API credentials (appId, appKey) are correct

Last updated on Apr 04, 2026