Linkzly

iOS SDK Setup Guide (Swift)

---

9 min read

sidebar_label: iOS SDK (Swift) sidebar_category: Gaming SDK Guides

iOS SDK Setup Guide (Swift)

This guide covers integrating Gaming Intelligence into an iOS app using Swift. There is no dedicated CocoaPods or Swift Package Manager package for iOS — integration is done by calling the Gaming Intelligence REST API directly using Swift's built-in URLSession. This approach gives you full control over request timing and network configuration.


Prerequisites

Before you begin, have the following ready:

Requirement Where to Find It
SDK Key Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key
Organization ID Linkzly Console → Organization Settings
Game ID Linkzly Console → Gaming → [Your Game] → Settings → General Settings
iOS Deployment Target iOS 13.0 or later
Xcode Xcode 14 or later

Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.


Setup

Create a new Swift file in your project named LinkzlyGamingClient.swift. This helper class wraps the REST API call and handles the required authentication headers.

import Foundation

class LinkzlyGamingClient {

    private let sdkKey: String
    private let organizationId: String
    private let gameId: String
    private let baseURL: String
    private let session: URLSession

    init(
        sdkKey: String,
        organizationId: String,
        gameId: String,
        baseURL: String = "https://gaming.linkzly.com"
    ) {
        self.sdkKey = sdkKey
        self.organizationId = organizationId
        self.gameId = gameId
        self.baseURL = baseURL

        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        self.session = URLSession(configuration: config)
    }

    /// Send an array of events to the Linkzly ingestion endpoint.
    func sendEvents(_ events: [[String: Any]], completion: ((Result<BatchResponse, Error>) -> Void)? = nil) {
        guard let url = URL(string: "\(baseURL)/api/v1/gaming/events") else { return }

        let payload: [String: Any] = [
            "batch_id": UUID().uuidString,
            "batch_timestamp": ISO8601DateFormatter().string(from: Date()),
            "events": events
        ]

        guard let body = try? JSONSerialization.data(withJSONObject: payload) else { return }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = body
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(sdkKey)", forHTTPHeaderField: "Authorization")
        request.setValue(organizationId, forHTTPHeaderField: "X-Organization-ID")
        request.setValue(gameId, forHTTPHeaderField: "X-Game-ID")

        session.dataTask(with: request) { data, response, error in
            if let error = error {
                completion?(.failure(error))
                return
            }
            guard let data = data,
                  let result = try? JSONDecoder().decode(BatchResponse.self, from: data) else {
                completion?(.failure(URLError(.cannotParseResponse)))
                return
            }
            completion?(.success(result))
        }.resume()
    }
}

Event Payload Struct

Define a Codable struct for type-safe event construction:

struct GameEvent: Encodable {
    let event_id: String
    let event_type: String
    let timestamp: String
    let platform: String
    let player_id: String
    let session_id: String
    let data: [String: AnyCodable]?

    init(
        eventType: String,
        playerId: String,
        sessionId: String,
        data: [String: Any]? = nil
    ) {
        self.event_id = UUID().uuidString
        self.event_type = eventType
        self.timestamp = ISO8601DateFormatter().string(from: Date())
        self.platform = "ios"
        self.player_id = playerId
        self.session_id = sessionId
        self.data = data?.mapValues { AnyCodable($0) }
    }
}

struct BatchResponse: Decodable {
    let batch_id: String
    let events_received: Int
    let events_valid: Int
    let events_dropped: Int
    let trace_id: String
    let server_timestamp: String
}

Note on AnyCodable: The data field is a free-form dictionary. To encode it cleanly, use a third-party AnyCodable type-eraser (available as a lightweight package) or serialize data separately with JSONSerialization and merge it into the payload dictionary before sending.


Sending Events

Instantiate the client once (for example, as a property on your AppDelegate or a singleton) and call sendEvents(_:) with an array of event dictionaries:

// Instantiate once (e.g., in AppDelegate or a GameSession manager)
let gaming = LinkzlyGamingClient(
    sdkKey: "YOUR_SDK_KEY",
    organizationId: "YOUR_ORG_ID",
    gameId: "YOUR_GAME_ID"
)

// Track session start
let sessionId = UUID().uuidString
gaming.sendEvents([[
    "event_id": UUID().uuidString,
    "event_type": "session_start",
    "timestamp": ISO8601DateFormatter().string(from: Date()),
    "platform": "ios",
    "player_id": "player-001",
    "session_id": sessionId,
    "data": [:]
]]) { result in
    switch result {
    case .success(let response):
        print("Batch accepted: \(response.batch_id), valid: \(response.events_valid)")
    case .failure(let error):
        print("Send failed: \(error.localizedDescription)")
    }
}

// Track a custom event
gaming.sendEvents([[
    "event_id": UUID().uuidString,
    "event_type": "level_complete",
    "timestamp": ISO8601DateFormatter().string(from: Date()),
    "platform": "ios",
    "player_id": "player-001",
    "session_id": sessionId,
    "data": [
        "level_id": "level-3",
        "score": 8500,
        "duration_seconds": 95,
        "stars": 2
    ]
]])

Batching Multiple Events

Send multiple events in a single request to reduce network overhead. The ingestion endpoint accepts up to 1,000 events per batch:

gaming.sendEvents([
    [
        "event_id": UUID().uuidString,
        "event_type": "currency_earned",
        "timestamp": ISO8601DateFormatter().string(from: Date()),
        "platform": "ios",
        "player_id": "player-001",
        "session_id": sessionId,
        "data": ["amount": 100, "currency_type": "gold", "source": "level_reward"]
    ],
    [
        "event_id": UUID().uuidString,
        "event_type": "item_purchased",
        "timestamp": ISO8601DateFormatter().string(from: Date()),
        "platform": "ios",
        "player_id": "player-001",
        "session_id": sessionId,
        "data": ["item_id": "shield-basic", "cost": 50, "currency_type": "gold"]
    ]
])

Session Management

There is no automatic session lifecycle management in the direct URLSession approach. Handle session start and end by hooking into the UIApplicationDelegate lifecycle:

// AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {

    var gaming: LinkzlyGamingClient!
    var currentSessionId: String = UUID().uuidString
    let playerId = "player-001" // Replace with your actual player ID after login

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        gaming = LinkzlyGamingClient(
            sdkKey: "YOUR_SDK_KEY",
            organizationId: "YOUR_ORG_ID",
            gameId: "YOUR_GAME_ID"
        )
        sendSessionStart()
        return true
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        sendSessionEnd()
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        currentSessionId = UUID().uuidString // New session on foreground
        sendSessionStart()
    }

    private func sendSessionStart() {
        gaming.sendEvents([[
            "event_id": UUID().uuidString,
            "event_type": "session_start",
            "timestamp": ISO8601DateFormatter().string(from: Date()),
            "platform": "ios",
            "player_id": playerId,
            "session_id": currentSessionId,
            "data": [:]
        ]])
    }

    private func sendSessionEnd() {
        gaming.sendEvents([[
            "event_id": UUID().uuidString,
            "event_type": "session_end",
            "timestamp": ISO8601DateFormatter().string(from: Date()),
            "platform": "ios",
            "player_id": playerId,
            "session_id": currentSessionId,
            "data": [:]
        ]])
    }
}

SwiftUI apps: If your app uses the @main lifecycle without AppDelegate, use scenePhase in your root App struct instead:

@Environment(\.scenePhase) private var scenePhase

.onChange(of: scenePhase) { phase in
    if phase == .active   { sendSessionStart() }
    if phase == .background { sendSessionEnd() }
}

HMAC Request Signing

If HMAC Signing is enabled for your game (the default for new games), every request must include a valid signature. Compute it in Swift using CryptoKit:

import CryptoKit

extension LinkzlyGamingClient {

    func sendEventsWithHMAC(
        _ events: [[String: Any]],
        signingSecret: String,
        completion: ((Result<BatchResponse, Error>) -> Void)? = nil
    ) {
        guard let url = URL(string: "\(baseURL)/api/v1/gaming/events") else { return }

        let payload: [String: Any] = [
            "batch_id": UUID().uuidString,
            "batch_timestamp": ISO8601DateFormatter().string(from: Date()),
            "events": events
        ]

        guard let body = try? JSONSerialization.data(withJSONObject: payload) else { return }

        // Compute HMAC signature
        let timestamp = String(Int64(Date().timeIntervalSince1970 * 1000))
        let nonce = UUID().uuidString
        guard let bodyString = String(data: body, encoding: .utf8) else { return }

        let signingString = "\(timestamp).\(nonce).\(bodyString)"
        let key = SymmetricKey(data: Data(signingSecret.utf8))
        let signature = HMAC<SHA256>.authenticationCode(
            for: Data(signingString.utf8),
            using: key
        )
        let hexSignature = signature.map { String(format: "%02x", $0) }.joined()

        // Build the request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = body
        request.setValue("application/json",  forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(sdkKey)",  forHTTPHeaderField: "Authorization")
        request.setValue(organizationId,       forHTTPHeaderField: "X-Organization-ID")
        request.setValue(gameId,               forHTTPHeaderField: "X-Game-ID")
        request.setValue(hexSignature,         forHTTPHeaderField: "X-Signature-256")
        request.setValue(timestamp,            forHTTPHeaderField: "X-Timestamp")
        request.setValue(nonce,                forHTTPHeaderField: "X-Nonce")

        session.dataTask(with: request) { data, response, error in
            if let error = error { completion?(.failure(error)); return }
            guard let data = data,
                  let result = try? JSONDecoder().decode(BatchResponse.self, from: data) else {
                completion?(.failure(URLError(.cannotParseResponse))); return
            }
            completion?(.success(result))
        }.resume()
    }
}

How the signature is computed:

  1. Build the signing string: "{timestamp}.{nonce}.{rawBodyString}"
  2. Compute HMAC-SHA256 of that string using your signing secret as the key.
  3. Hex-encode the resulting bytes.
  4. Send the result in the X-Signature-256 header.

The replay window is fixed at 300 seconds. Requests with a timestamp more than 5 minutes out of sync with the server are rejected with 401 Unauthorized. Ensure the device clock is synchronized.

Security note: Do not hardcode your signing secret in source files committed to version control. Store it in your app's keychain or inject it at build time using Xcode build settings.


Error Handling

Check the HTTP status code in the URLSession completion handler before processing the response:

session.dataTask(with: request) { data, response, error in
    if let error = error {
        // Network error (no connectivity, timeout, etc.)
        print("Network error: \(error.localizedDescription)")
        return
    }

    guard let httpResponse = response as? HTTPURLResponse else { return }

    switch httpResponse.statusCode {
    case 202:
        // Success — batch accepted
        break
    case 401:
        // Invalid SDK key, wrong org/game ID, or HMAC signature failure
        print("Authentication error — check your SDK key and signing secret")
    case 429:
        // Rate limited — back off and retry
        let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After") ?? "60"
        print("Rate limited — retry after \(retryAfter) seconds")
    case 400:
        // Malformed request — check event payload structure
        if let data = data, let body = String(data: data, encoding: .utf8) {
            print("Bad request: \(body)")
        }
    default:
        print("Unexpected status: \(httpResponse.statusCode)")
    }
}.resume()

Background Sending

For greater reliability — especially for session_end events sent when the app enters the background — use a background URLSession configuration. Background sessions continue their tasks even after the app is suspended:

let backgroundConfig = URLSessionConfiguration.background(
    withIdentifier: "com.yourapp.linkzly-gaming"
)
backgroundConfig.timeoutIntervalForRequest = 30
let backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)

Implement URLSessionDelegate to handle completion events when the system wakes your app to deliver results.


Troubleshooting

No events appearing in the dashboard

  1. Confirm sdkKey, organizationId, and gameId are all correct (no trailing whitespace).
  2. Verify your app has network permission — check NSAllowsArbitraryLoads is not blocking HTTPS in Info.plist.
  3. Use Charles Proxy or the Xcode Network Instruments tool to inspect the outgoing request and response.

401 Unauthorized with HMAC enabled

  1. Confirm the signing secret matches the value in the Linkzly console under Game Settings → SDK Configuration.
  2. Check that the device clock is within 5 minutes of UTC. The replay window is fixed at 300 seconds.
  3. Ensure the body is serialized to bytes before computing the signature, and that the same byte representation is sent as the HTTP body.

Events dropped (events_dropped > 0)

Check that each event includes all required fields: event_id, event_type, timestamp (ISO 8601), platform, player_id, and session_id. Events with missing required fields are dropped silently.

Was this helpful?

Help us improve our documentation