docs
Documentation menu

iOS SDK reference

The Swift API surface — types, methods, and options.

#Installation

The Pushlane iOS SDK requires iOS 16+ and is distributed as a Swift Package (swift-tools-version 5.9). Add it in Xcode via File › Add Package Dependencies, or add it to your Package.swift:

swift
// Package.swift — add to dependencies:
.package(url: "https://github.com/Big-Terence/pushlane-ios-sdk", from: "0.1.0")

// Add to your app target:
.product(name: "PushlaneCore",  package: "pushlane-ios-sdk"),
.product(name: "PushlanePush",  package: "pushlane-ios-sdk"),
.product(name: "PushlaneInApp", package: "pushlane-ios-sdk"),

// Add to your NotificationServiceExtension target only:
.product(name: "PushlaneNotificationService", package: "pushlane-ios-sdk"),
ProductAdd to targetPurpose
PushlaneCoreApp targetCore facade: configure, identify, track. Pure Foundation, no UIKit.
PushlanePushApp targetPush registration and open-tracking.
PushlaneInAppApp targetSession tracking. Emits app_open and session_started.
PushlaneNotificationServiceNSE target onlyBase class for your NotificationServiceExtension. Handles rich media and the received event.

#Setup order

Wire the SDK in four calls at launch. Order matters — configure before identify, identify before track or register.

swift
import PushlaneCore
import PushlanePush
import PushlaneInApp

@main
struct MyApp: App {
    init() {
        // 1. Configure — call before any track/identify call.
        Pushlane.configure(
            apiBase:        URL(string: "https://<YOUR_WORKER>")!,
            tenantId:       "YOUR_TENANT_UUID",
            publishableKey: "lpk_live_…",             // from dashboard › Settings › API
            appGroup:       "group.com.example.myapp" // optional; needed for received events
        )

        // 2. Identify — set the user id before tracking events.
        if let userId = Auth.current?.id {
            Pushlane.identify(userId)
        }

        // 3. Session tracking + app_open event (once per cold launch).
        PushlaneInApp.start()

        // 4. Request push permission and register with APNs.
        PushlanePush.register()
    }
}

#Pushlane.configure

ParameterTypeNotes
apiBaseURLBase URL of your ingest worker. No trailing slash. Available in dashboard › Settings › API.
tenantIdString (UUID)Your tenant UUID from dashboard › Settings › API.
publishableKeyString? (lpk_live_…)Sent as Authorization: Bearer on every request. Required when INGEST_REQUIRE_KEY is on (default in production).
appGroupString?Shared App Group id. Required for the received deliverability event from the NSE. Must match PushlaneNotificationService.pushlaneAppGroup.

The SDK auto-detects the APNs routing environment (sandbox vs. production) from the embedded provisioning profile and the install source (appstore / testflight / development) from the App Store receipt name. You never set these manually.

#Pushlane.identify / Pushlane.reset

Heads up
Call Pushlane.identify() before PushlanePush.register() and before the first Pushlane.track() call. The SDK warns once in the Xcode console if a device token or a track call arrives before an identity is set, and the data is silently dropped until an id is available.
swift
// Set (or update) the current user — call as soon as the user id is known.
Pushlane.identify("user_42")

// Log out — detach the external id from this device.
Pushlane.reset()

When appGroup is configured, Pushlane.identify mirrors the external id into the App Group so the NotificationServiceExtension (a separate OS process) can attribute the received event to the correct user.

#Pushlane.track

Send a named event with optional typed properties. The call is fire-and-forget — it returns immediately and retries up to 4 times with exponential backoff on 5xx or network errors.

swift
// Zero properties
Pushlane.track("paywall_dismissed")

// With properties
Pushlane.track("paywall_viewed", [
    "placement":  "onboarding",
    "paywall_id": "main",
])

// Mixed types
Pushlane.track("subscription_started", [
    "plan":      "annual",
    "price_usd": 49.99,
    "is_trial":  false,
])

Property values are typed as PushlaneValue. Swift literal syntax works directly in dictionary literals:

swift
// Explicit construction
PushlaneValue.string("hello")
PushlaneValue.int(42)
PushlaneValue.double(3.14)
PushlaneValue.bool(true)
PushlaneValue.array([.string("a"), .string("b")])

// Literal shorthand (inferred automatically in dictionary literals)
let props: [String: PushlaneValue] = [
    "name":  "Alice",  // .string
    "score": 100,      // .int
    "ratio": 0.5,      // .double
    "paid":  true,     // .bool
]
Note
The backend validates property types against your declared catalogue. Unknown properties land in raw storage and are visible in the Logs view. Pushlane never infers a type from the first value it sees — declare property types in the catalogue to get full coercion and warnings.

#Push registration

Call PushlanePush.register() once at launch. It requests notification permission and calls UIApplication.shared.registerForRemoteNotifications() on grant. Forward both AppDelegate callbacks:

swift
// AppDelegate.swift (UIKit) or Scene lifecycle equivalent

func application(
    _ app: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken token: Data
) {
    PushlanePush.didRegister(deviceToken: token)
}

func application(
    _ app: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: Error
) {
    PushlanePush.didFailToRegister(error: error)
}

PushlanePush.didRegister(deviceToken:) posts the hex-encoded token, the auto-detected APNs environment, and the current app and OS versions to /v1/register.

Heads up
APNs environment is auto-detected from embedded.mobileprovision, not from #if DEBUG. TestFlight builds are compiled in Release mode but use the sandbox APNs gateway. The SDK handles this correctly; a #if DEBUG check does not.

Open tracking is installed automatically when you call PushlanePush.register(). The SDK becomes the UNUserNotificationCenter delegate and emits an opened event carrying message_id, flow_id, and node_id from the push payload on every tap, including cold-launch taps.

#Session tracking

PushlaneInApp.start() must be called once at launch (after configure and identify). It does two things:

  • Emits app_open exactly once per cold launch of the process. Not re-emitted on foreground return.
  • Hooks UIKit lifecycle notifications. When the app returns to the foreground after 30 seconds or more in the background, it emits session_started with a fresh session_id.

Both events carry SDK version, OS version, app version, APNs environment, install source, timezone, and locale automatically via the event context.

#Deliverability — the received event

received is emitted by the NotificationServiceExtension — a separate OS process that intercepts push payloads before they are displayed. It fires even when the user never opens the notification, giving you delivery data independent of open rates.

Create a NotificationServiceExtension target in Xcode, then subclass PushlaneNotificationService:

swift
// NotificationService.swift — in your NSE Xcode target
import PushlaneNotificationService

final class NotificationService: PushlaneNotificationService {
    // Return the SAME App Group id you passed to Pushlane.configure(appGroup:)
    override var pushlaneAppGroup: String? {
        "group.com.example.myapp"
    }
}
xml
<!-- NSE target Info.plist — present in the Xcode template -->
<key>NSExtension</key>
<dict>
  <key>NSExtensionPointIdentifier</key>
  <string>com.apple.usernotifications.service</string>
  <key>NSExtensionPrincipalClass</key>
  <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
Note
The NSE target must share an App Group with the app target. Add the App Groups capability to both in Xcode and use the same id in both Pushlane.configure(appGroup:) and pushlaneAppGroup. Without the shared App Group, rich media download still works but no received event is emitted.

PushlaneNotificationService also downloads the attachment URL from the image_url or loop_media key and attaches it as a UNNotificationAttachment, with correct file extension inference from the MIME type.

Pushlane attributes RevenueCat revenue events (trial started, renewal, cancellation, etc.) to a Pushlane user only when RevenueCat's appUserID exactly matches the id passed to Pushlane.identify. Use the convenience method to keep both in sync at a single call site:

swift
// Identify for Pushlane AND pass the same id to RevenueCat.
// Both SDKs must use the SAME userId or revenue events won't reach your flows.
let uid = Pushlane.identifyForRevenueCat(currentUser.id)
Purchases.configure(withAPIKey: "appl_…", appUserID: uid)

Pushlane.identifyForRevenueCat(_:) calls Pushlane.identify and returns the id unchanged. It has no compile-time dependency on the RevenueCat SDK.

#Auto-emitted events

These events are emitted by the SDK. Do not instrument them manually.

EventEmitted byWhen
app_openPushlaneInApp.start()Once per cold launch of the process.
session_startedPushlaneInApp (lifecycle observer)Foreground return after ≥30 s background.
openedPushlanePush (UNUserNotificationCenterDelegate)User taps a push notification (cold-launch or foreground).
receivedPushlaneNotificationService (NSE)Push payload intercepted by the NSE before display.
push_registration_failedPushlanePush.didFailToRegisterAPNs returned an error during registration.

Next: AI-agent install (MCP) →