Intempt Docs
Developer DocsSDK

Apple SDK

Track user interactions across iOS, iPadOS, tvOS, macOS and watchOS, deliver APNs push from journeys, and manage configuration from the Intempt console.

Apple SDK

The Intempt Apple SDK tracks user interactions in your app, resolves experiments and recommendations, records consent, and registers devices for push notifications sent from Intempt journeys.

One package covers every Apple platform. The page is still called the iOS SDK in most places because that is where nearly all traffic comes from, but nothing here is iOS-only except the autocapture hooks, which need UIKit.

This SDK replaces the Objective-C SDK (intempt-intemptios), which is no longer distributed. The API is not source-compatible — see Migrating from the Objective-C SDK.

Requirements

Swift5.9+
Xcode15.0+
iOS / iPadOS / tvOS15.0+
macOS12.0+
watchOS8.0+

The iOS floor is 15.0. A target still set to 14.x fails to build against the SDK with compiling for iOS 14.0, but module 'Intempt' has a minimum deployment target of iOS 15.0.

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/intempt/intempt-swift.git", from: "0.1.0")
]

In Xcode: File → Add Package Dependencies, enter https://github.com/intempt/intempt-swift.git, and choose Up to Next Major Version from 0.1.0.

CocoaPods

pod 'Intempt', '~> 0.1.0'

A cross-platform wrapper depends on the same pod:

s.dependency 'Intempt', '0.1.0'

Setup

Collect these from the Intempt console under Integrations → Sources, on the Apple source you are sending to:

ValueNotes
Organization nameThe orgId passed to initialize
Project nameThe projectId passed to initialize
Source IDNumeric. Also forms the push token attribute name
API keyOf the form prefix.secret

Initialization

import Intempt

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        do {
            let intempt = try IntemptInstance.initialize(
                apiKey: "yourPrefix.yourSecret",
                orgId: "your-org",
                projectId: "your-project",
                sourceId: "your-source-id"
            )

            intempt.autocapture.configure(.all)
            intempt.autocapture.start()
        } catch {
            // Blank identifiers and malformed keys throw here rather than failing silently.
            print("Intempt initialization failed: \(error)")
        }

        return true
    }
}

initialize is declared on IntemptInstance. The Intempt enum is a namespace for constants (sdkVersion, defaultFeedFields), not an entry point.

It throws rather than logging and continuing: a blank orgId is an integration error, and posting to a malformed URL forever is not a recovery. Calling it twice with the same instance name returns the existing instance instead of building a second one.

Reach the same instance later from anywhere:

guard let intempt = IntemptInstance.mainInstance() else { return }

SwiftUI

There is no AppDelegate in a pure SwiftUI lifecycle, so initialize from the App initializer and keep the instance yourself:

@main
struct MyApp: App {
    init() {
        _ = try? IntemptInstance.initialize(
            apiKey: "yourPrefix.yourSecret",
            orgId: "your-org",
            projectId: "your-project",
            sourceId: "your-source-id"
        )
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

Autocapture hooks UIViewController, so a pure SwiftUI view hierarchy produces few screen events. Track screens explicitly with track(eventTitle:) in onAppear.

Capture

track()

Records a custom event.

intempt.track(eventTitle: "flight-booking", data: [
    "flightId": 1,
    "bookingDate": "2024-01-29",
    "bookingStatus": "booked",
])

record()

One call that carries user and account context together.

intempt.record(
    eventTitle: "flight-booking",
    userId: "xyz@intempt.com",
    accountId: "intempt.com",
    data: ["flightId": 1],
    userAttributes: ["city": "New York", "country": "USA"],
    accountAttributes: ["mtuCount": "1000+"]
)

Every tracking method returns @discardableResult Boolfalse means the event was not queued, most often because the user has opted out.


Commerce

intempt.productView(productId: "2369423736890")
intempt.productAdd(productId: "2369423736890", quantity: 2)
intempt.productOrdered(products: [
    (productId: "2369423736890", quantity: 2),
    (productId: "2369423736891", quantity: 1),
])

intempt.consent(action: .accept, validUntil: 31_536_000)
intempt.consent(action: .reject, validUntil: 0, category: "marketing")
ParameterTypeDescription
actionConsentAction.accept or .reject
validUntilTimeIntervalSeconds the decision stays valid
emailString?Optional subject identifier
messageString?The exact wording the user agreed to
categoryString?Scopes the decision, e.g. marketing

.reject enforces the decision rather than only recording it: collection stops and queued events are purged, the same gate as optOut().

The withdrawal record itself is still transmitted, even while opted out. It goes to its own endpoint, unbatched — it is the evidence the user objected, and dropping it would leave the objection recorded nowhere.


Identity and lifecycle

MethodSetsSignature
identify()The useridentify(userId:eventTitle:userAttributes:data:)
group()The account the user belongs togroup(accountId:eventTitle:accountAttributes:)
alias()Links two identitiesalias(userId:anotherUserId:)
intempt.identify(userId: "xyz@intempt.com", userAttributes: [
    "city": "New York",
    "plan": "pro",
])

intempt.group(accountId: "intempt.com", accountAttributes: [
    "mtuCount": "1000+",
])

intempt.alias(userId: "xyz@intempt.com", anotherUserId: "legacy-id-42")

group takes accountAttributes. The Objective-C SDK called this parameter userAttributes while sending an account, which is worth knowing if you are porting old code across.

Read the current identity back:

MethodReturnsDescription
getProfileId()StringCurrent anonymous or identified profile
getSessionId()StringCurrent session
sdkVersionStringSDK version

Opt in / out

MethodReturnsDescription
isUserOptIn()BoolWhether collection is permitted
hasOptedOut()BoolThe inverse
optIn()VoidResume collection
optOut()VoidStop collecting and discard what is queued
logOut()VoidRotate to a fresh anonymous identity
reset()VoidNew identity and an empty queue

optOut() discards the queue as well as stopping collection. If you need the events already gathered, flush() before opting out.


Delivery

MethodReturnsDescription
flush()VoidSend everything queued now
flush { sent in }VoidCompletion carries the delivered count
flushIntervalTimeIntervalSeconds between automatic flushes; 0 disables the timer

Events are batched and flushed on a timer. flush { sent in } is the only way to observe delivery — the tracking calls return whether an event was queued, not whether it arrived.


Personalization

intempt.products(feedId: "9", count: 4, productId: "2369423736890") { result in
    switch result {
    case .success(let products): render(products)
    case .failure(let error):    print(error)
    }
}

fields: defaults to Intempt.defaultFeedFields on purpose. An unfielded request returns every catalog column, including raw ML embedding vectors — measured at 443 times the payload size. Widen it deliberately, never by omission.

Experiments and personalizations are not in this SDK. There is no server-side support for experiment assignment on any SDK — mobile or server-side — so no Intempt SDK exposes it. The assignment endpoint answers 200 with an empty set, which is indistinguishable from a profile that simply has no assignments, so a client could never branch on a result.

Recommendation feeds are a separate capability against a separate endpoint and are unaffected.

Automatic events

Two mechanisms produce events without call sites, and they are configured differently. Mixing them up is the usual reason an expected event never arrives. This is the first; Autocapture is the second.

Lifecycle events. A settable property, no start call, and sessions are already on.

EventEmitted whenOption
Session startA new session beginssessions (on by default)
Session end5 minutes of inactivity, or the app closessessions (on by default)
App Install/UpgradeFirst launch after an install or a version changeversionChanges
Application OpenedThe app enters the foregroundappStateChanges
Application BackgroundedThe app leaves itappStateChanges
intempt.automaticEvents = AutomaticEventOptions(
    sessions: true,
    versionChanges: true,
    appStateChanges: true
)

Autocapture

UI interactions. Needs configure(_:) and start(), and everything is off until you call them, because it works by swizzling UIKit.

EventEmitted whenOption
View screenA UIViewController appearsscreens
Leave screenA UIViewController disappears, carrying how long it was visiblescreenExits
ActionA UIControl fires an action — a button presstaps
Edit FieldA UIControl value changes — a switch, slider or text fieldcontrolChanges
TouchA tap that does not land on a UIControlrawTouches

Autocapture is iOS and tvOS only; it needs UIKit.

taps and rawTouches are deliberately separate. A button press already produces an Action, so counting it as a Touch as well would double-count every press.

Push notifications

Intempt sends push straight to APNs. There is no Firebase or FCM dependency at any layer, on the device or on the server.

1. Register the device

func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    IntemptInstance.mainInstance()?.setPushToken(deviceToken)
}

Pass the raw Data. Do not stringify it first — since iOS 13 the widespread deviceToken.description idiom produces the literal string "32 bytes", not a token. setPushToken rejects anything too short to be an APNs token and returns false rather than registering it.

The token is sent as a user attribute named apns_token_<sourceId> on an App Install/Upgrade event. The per-source name is deliberate: one device can be registered against several sources, and a flat name would collide.

2. Attribute opens and receipts

// UNUserNotificationCenterDelegate — the user tapped the notification
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    IntemptInstance.mainInstance()?.trackPushOpen(response.notification.request.content.userInfo)
    completionHandler()
}

// Silent or foreground arrival
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
    IntemptInstance.mainInstance()?.trackPushReceived(userInfo)
    completionHandler(.newData)
}

Only the campaign identifier and the developer-authored title are read from the payload. The notification body is never collected.

3. Configure the credentials in Intempt

Push is delivered with a token-based APNs key (.p8), not a certificate — one key works for every app in your team and does not expire annually.

FieldWhere it comes fromNotes
Signing keyApple Developer → Keys → a key with Apple Push Notifications service enabledThe .p8 file. Downloadable only once
Key IDShown beside the keyExactly 10 alphanumeric characters
Team IDApple Developer → MembershipExactly 10 alphanumeric characters
Bundle IDYour app's bundle identifierBecomes the APNs topic
SandboxToggleOn for builds signed with a development profile

Two mistakes account for most failures, and Apple reports both as the same opaque error hours later:

  • Pasting the key filename into Key ID. AuthKey_ABC1234567.p8 contains the right value but is not it. Intempt rejects this at save time.
  • Wrong environment. A token minted by a development build is not valid against production APNs, and the resulting 400 BadDeviceToken is indistinguishable from a malformed token. Match the Sandbox toggle to how the build was signed.

4. Send from a journey

Add the Apple push destination to a journey. It resolves each profile's apns_token_<sourceId> attribute, so a device only becomes reachable after the app has called setPushToken at least once and the event has flushed.


Configuration reference

Everything configurable, and where it lives. Unlike the server SDKs there is no options object — initialize takes credentials only, and behaviour is set on the instance afterwards.

settingtypedefaultwhat it does
apiKeyStringa public key, <prefix>.<secret>
orgIdStringyour organization name, from the console URL
projectIdStringyour project name, from the console URL
sourceIdStringthe source events are attributed to
instanceNameString"default"names the instance. mainInstance() returns the "default" one
flushIntervalTimeInterval60seconds between automatic flushes. 0 disables the timer
automaticEventsAutomaticEventOptionssessions on, others offlifecycle events — see Automatic events
autocaptureAutocaptureOptionsall offUI interactions. Installs nothing until start()
let intempt = try IntemptInstance.initialize(
    apiKey: "yourPrefix.yourSecret",
    orgId: "your-org",
    projectId: "your-project",
    sourceId: "your-source-id")

intempt.flushInterval = 30
intempt.automaticEvents = AutomaticEventOptions(sessions: true, versionChanges: true)
intempt.autocapture.configure(.all)
intempt.autocapture.start()

Logging

Off by default. Nothing is written to the console unless you ask for it.

IntemptLogger.shared.addLogging(IntemptPrintLogger())
IntemptLogger.shared.enableAllLevels()          // or .enable(.warning)

Levels are .debug, .info, .warning, .error. The logger never writes API keys or event bodies.

Queue limits

Queue ceiling5,000 events — beyond that the oldest are evicted
Batch deletiononly after the server acknowledges; never before the request
StorageSQLite WAL under Library/, excluded from backup

Privacy manifest

The SDK ships PrivacyInfo.xcprivacy inside its bundle, which Apple requires of a third-party SDK. You do not need to add one for Intempt — but you do need to account for what it declares when you fill in your own App Store privacy answers.

DeclarationValue
NSPrivacyTrackingfalse — the SDK does not track across apps or websites
Collected data typesProduct interaction, User ID, Device ID
Accessed API — UserDefaultsreason CA92.1 (access to the app's own defaults)
Accessed API — disk spacereason E174.1 (writing the event queue)

NSPrivacyTracking is false because nothing here joins a user across other companies' apps. If your app links Intempt data to a third-party advertising identifier, that is your declaration to make, not this one.


Property types

Event and attribute values conform to IntemptType: String, Int, UInt, Double, Float, Bool, Date, URL, NSNull, NSNumber, and arrays or dictionaries of the same, nested to any depth.

Values that cannot survive the wire — NaN, infinity — are rejected at the call boundary rather than serialized as the string "nan".

Data on disk

Events are queued in SQLite (WAL mode) under Library/, excluded from iCloud and iTunes backup, with data protection set to COMPLETEUNTILFIRSTUSERAUTHENTICATION so a background flush cannot hang on a locked screen.

The queue is capped at 5,000 events; beyond that the oldest are evicted, so an offline device cannot fill the user's disk. A batch is deleted only after the server acknowledges it — never before the request, and never as a blanket delete.


Errors and troubleshooting

IssueSolution
no such module 'Intempt' when resolving the packageClean the package cache (File → Packages → Reset Package Caches) and confirm the target actually links the Intempt product
compiling for iOS 14.0, but module 'Intempt' has a minimum deployment target of iOS 15.0Raise the target's iOS Deployment Target to 15.0
setPushToken returns falseYou passed a stringified token. Pass the raw Data from the registration callback
Push never arrives, no errorThe device has not registered. Confirm setPushToken ran and the event flushed, then check the profile carries apns_token_<sourceId>
APNs reports BadDeviceTokenThe Sandbox toggle does not match how the build was signed
No autocapture eventsAutocapture is opt-in — call configure(...) and start()
No data in the consoleEvents batch periodically. Call flush { sent in } to send now and read the count
Events stop after a consent prompt.reject enforces the decision and purges the queue. Call optIn() or record an .accept
Field type errorsMatch schema types exactly. New fields are fine; the type of an existing field cannot change

Migrating from the Objective-C SDK

Objective-C SDKApple SDK
Intempt.initialize(_:projectName:sourceId:apiKey:intemptConfig:)IntemptInstance.initialize(apiKey:orgId:projectId:sourceId:)
IntemptConfig(queueEnabled:withItemsInQueue:...)flushInterval; batching is automatic
Completion handler on every callSynchronous Bool; flush { sent in } for delivery
Intempt.track(_:data:completion:)track(eventTitle:data:)
chooseExperiments(...) / choosePersonalizations(...)removed — no server-side experiment support on mobile
recommendation(_:fields:quantity:productId:)products(feedId:count:fields:productId:)
enableLogging() / disableLogging()IntemptLogger levels
startTrackingSession() / endTrackingSession()Managed automatically
disableTextInput(true)AutocaptureOptions.controlChanges = false

Data types are unchanged: [String: Any] dictionaries become [String: IntemptType], which accepts the same values with compile-time checking.


On this page