For production, prefer the full client. It collects more signals, applies SKAdNetwork conversion values, and retries failed requests, which improves match rates. Use this minimal version only when you want the smallest possible footprint.
LinkrunnerMinimal.swift
Set your project token at the top. No third-party dependencies are needed.import Foundation
import UIKit
import AdSupport
import AppTrackingTransparency
import AdServices
@available(iOS 15.0, *)
enum LinkrunnerMinimal {
private static let baseURL = "https://api.linkrunner.io"
private static let token = "YOUR_PROJECT_TOKEN"
private static let installIdKey = "linkrunner_min_iid"
/// Generated once, then reused for the life of the install.
private static func installId() -> String {
if let id = UserDefaults.standard.string(forKey: installIdKey) { return id }
let id = UUID().uuidString
UserDefaults.standard.set(id, forKey: installIdKey)
return id
}
private static func base() -> [String: Any] {
["token": token, "platform": "IOS", "install_instance_id": installId()]
}
private static func post(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
var request = URLRequest(url: URL(string: baseURL + path)!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
guard (200..<300).contains(code) else {
throw NSError(domain: "Linkrunner", code: code,
userInfo: [NSLocalizedDescriptionKey: String(data: data, encoding: .utf8) ?? ""])
}
return (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:]
}
private static func deviceData() async -> [String: Any] {
var d: [String: Any] = ["brand": "Apple", "manufacturer": "Apple"]
if let idfv = await MainActor.run(body: { UIDevice.current.identifierForVendor?.uuidString }) {
d["idfv"] = idfv
}
if ATTrackingManager.trackingAuthorizationStatus == .authorized {
d["idfa"] = ASIdentifierManager.shared().advertisingIdentifier.uuidString
}
if let token = try? AAAttribution.attributionToken() {
d["adservices_attribution_token"] = token
}
return d
}
// MARK: - Endpoints
@discardableResult
static func initialize() async throws -> [String: Any] {
var body = base()
body["device_data"] = await deviceData()
return try await post("/api/client/init", body)
}
static func attribution() async throws -> [String: Any] {
var body = base()
body["device_data"] = await deviceData()
return try await post("/api/client/attribution-data", body)
}
@discardableResult
static func signup(userId: String) async throws -> [String: Any] {
var body = base()
body["user_data"] = ["id": userId]
body["data"] = ["device_data": await deviceData()]
return try await post("/api/client/trigger", body)
}
@discardableResult
static func trackEvent(_ name: String, data: [String: Any]? = nil) async throws -> [String: Any] {
var body = base()
body["event_name"] = name
if let data { body["event_data"] = data }
return try await post("/api/client/capture-event", body)
}
@discardableResult
static func capturePayment(userId: String, amount: Double) async throws -> [String: Any] {
var body = base()
body["user_id"] = userId
body["amount"] = amount
body["status"] = "PAYMENT_COMPLETED"
return try await post("/api/client/capture-payment", body)
}
}
Using it
Register the install and read attribution on app start.import SwiftUI
@main
struct MyApp: App {
init() {
Task {
// 1. Register the install
try? await LinkrunnerMinimal.initialize()
// 2. Read attribution (poll until campaign_data appears)
for _ in 0..<5 {
let data = (try? await LinkrunnerMinimal.attribution())?["data"] as? [String: Any]
if let campaign = data?["campaign_data"] as? [String: Any] {
print("Attributed: \(campaign)")
break
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
}
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
// 3. When the user signs up or logs in
Task { try? await LinkrunnerMinimal.signup(userId: "user_123") }
// 4. Track an event
Task { try? await LinkrunnerMinimal.trackEvent("purchase_initiated", data: ["amount": 99.99]) }
// 5. Capture a payment
Task { try? await LinkrunnerMinimal.capturePayment(userId: "user_123", amount: 99.99) }
Call from the device so Linkrunner sees the device IP. To collect the IDFA, request App Tracking Transparency first (see the full client).