Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kontext.so/llms.txt

Use this file to discover all available pages before exploring further.

Swift SDK

See how easy it is to integrate high-performance ads into your iOS app using our lightweight SDK.

Requirements

  • iOS 14.0+
  • Swift 5.9+
  • Xcode 15+

Getting started

1. Installation

You’ll need a publisher account to obtain your publisherToken and placement codes.
Add the SDK as a package dependency:
dependencies: [
    .package(url: "https://github.com/kontextso/sdk-swift", .upToNextMajor(from: "4.0.0"))
]
Then add the KontextSwiftSDK product to your target.

2. Create a session

The entry point is KontextAds.createSession(_:), which returns a Session you’ll use for the rest of the conversation lifecycle.
import KontextSwiftSDK

let session = KontextAds.createSession(SessionOptions(
    publisherToken: "<your-publisher-token>",
    userId: "user-1234",
    conversationId: "conv-5678",
    character: Character(
        id: "character-1234",
        name: "John Doe",
        avatarUrl: URL(string: "https://example.com/avatar.png")!,
        greeting: "Hello, how can I help you today?"
    ),
    advertisingId: nil, // optional — pass an IDFA you collected manually (SDK auto-collects when nil)
    vendorId: nil, // optional — pass an IDFV you collected manually (SDK auto-collects when nil)
    onEvent: { event in
        // Handle ad lifecycle events
        print("[kontext] \(event.name)")
    }
))
The session keeps userId, conversationId, and publisherToken fixed for its lifetime. Recreate the session when any of those change (e.g. when the user starts a new chat).
Session is @MainActor — call its methods from the main actor (default in SwiftUI views and @MainActor-isolated view controllers).

3. Set up the IDFA (App Tracking Transparency)

The SDK reads the IDFA when the user has granted ATT permission. Add the usage description to Info.plist:
<key>NSUserTrackingUsageDescription</key>
<string>We use your advertising identifier to show you more relevant ads.</string>
Without this key, the app will crash on iOS 14+ when requesting ATT.
By default, the SDK calls ATTrackingManager.requestTrackingAuthorization automatically. To trigger it later (e.g. after onboarding) or manage the prompt yourself:
let session = KontextAds.createSession(SessionOptions(
    publisherToken: "<your-publisher-token>",
    userId: "user-1234",
    conversationId: "conv-5678",
    advertisingId: resolvedIDFA,             // optional — IDFA you collected manually
    vendorId: resolvedIDFV,                  // optional — IDFV you collected manually
    requestTrackingAuthorization: false      // skip the SDK's automatic prompt
))
advertisingId and vendorId are both optional. Leave them unset to let the SDK collect them (IDFA via ATTrackingManager when authorized, IDFV via UIDevice.current.identifierForVendor). Pass them only when you already collect those identifiers yourself.
The ATT prompt only appears when the app is active. Initializing the session in AppDelegate.didFinishLaunchingWithOptions may suppress the prompt — initialize after the app becomes active.

4. Set up SKAdNetwork

The SDK reads SKAdNetworkItems from Info.plist and forwards them on every /init so DSPs can measure conversions. Append the identifiers provided during onboarding:
<key>SKAdNetworkItems</key>
<array>
  <dict>
    <key>SKAdNetworkIdentifier</key>
    <string>XXX.skadnetwork</string>
  </dict>
  <!-- Add all provided identifiers here -->
</array>
If your app already has an SKAdNetworkItems array, append — don’t replace — or you’ll break attribution for other networks.

5. Feed conversation messages

Add every message to the session as it appears. User messages trigger a debounced preload in the background; assistant messages let the SDK assign the matched bid to the corresponding placement.
session.addMessage(Message(
    id: "msg-1",
    role: .user,
    content: "Hello, how are you?",
    createdAt: Date()
))

session.addMessage(Message(
    id: "msg-2",
    role: .assistant,
    content: "I am good, thank you!",
    createdAt: Date()
))
addMessage returns synchronously. The preload result is delivered later via the onEvent callback (.filled, .noFill, .error, …) — not via a return value.

6. Render the ad

Use session.createAd(messageId:) to obtain an Ad for an assistant message, then render it with InlineAdUIView. createAd is idempotent: calling it repeatedly with the same messageId returns the same Ad. Cache the returned instance so view-controller / cell reuse doesn’t recreate it.
let ad = session.createAd("msg-2")
let adView = InlineAdUIView(ad: ad)
adView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(adView)

// Inside a UITableView/UICollectionView cell, observe the height for proper sizing.
adView.onHeightChange = { [weak self] height in
    self?.updateRowHeight(height)
}
See the UIKit example app for a full reference integration.

7. Tear down

Call destroy() when the conversation ends or the view disappears. Idempotent and required to cancel pending network requests and release web view resources.
session.destroy()

Trying the example app

The Example directory in the SDK repo ships a working UIKit demo you can run end-to-end against your own publisher token.
git clone https://github.com/kontextso/sdk-swift.git
cd sdk-swift
cp ExampleSecrets.swift.example ExampleSecrets.swift
# Edit ExampleSecrets.swift and set `publisherToken` to your real token.
open Example/Example.xcodeproj
Then run the Example scheme on a simulator or device.
ExampleSecrets.swift lives at the repo root (not inside Example/) and is gitignored, so your token won’t be committed. The Xcode project references it at ../../ExampleSecrets.swift; if the file is missing the build fails clearly — copy the .example template before opening Xcode.
The default publisherToken placeholder (YOUR_PUBLISHER_TOKEN) won’t return real ads — replace it with your token from the publisher dashboard.

Observing events

Two equivalent ways to consume AdEvents — pick whichever fits your codebase:

onEvent callback

let session = KontextAds.createSession(SessionOptions(
    publisherToken: "...",
    userId: "...",
    conversationId: "...",
    onEvent: { event in
        switch event {
        case .filled(let data):
            print("ad filled: \(data.bidId) revenue=\(data.revenue ?? 0)")
        case .clicked(let data):
            print("clicked: \(data.url)")
        default: break
        }
    }
))

Combine publisher

import Combine

var cancellables: Set<AnyCancellable> = []

session.eventPublisher
    .sink { event in
        print("event: \(event.name)")
    }
    .store(in: &cancellables)
Both deliver the same events on the main thread.

Live-updating session options

A subset of session options can be live-updated without recreating the session. Read at the next /preload, so changes take effect on the next user message.
session.updateOptions(MutablePublisherOptions(
    variantId: "new-variant"
))
Live-updateable fields: variantId, regulatory, userEmail, advertisingId, vendorId. Non-nil fields overwrite; nil fields are left unchanged. To clear a field, recreate the session.
publisherToken, userId, conversationId, enabledPlacementCodes, and character are not live-updateable. Changing them mid-session would desync the /init registration or leave accumulated message history targeted at the wrong persona. Recreate the session instead.

SwiftUI

The SDK ships UIKit views only. Wrap InlineAdUIView in a UIViewRepresentable to embed it in SwiftUI:
import SwiftUI
import KontextSwiftSDK

struct InlineAd: UIViewRepresentable {
    let ad: Ad
    let onHeightChange: (CGFloat) -> Void

    func makeUIView(context: Context) -> InlineAdUIView {
        let view = InlineAdUIView(ad: ad)
        view.onHeightChange = onHeightChange
        return view
    }

    func updateUIView(_ uiView: InlineAdUIView, context: Context) {}
}

API reference

KontextAds.createSession(_:)

@MainActor
public static func createSession(_ options: SessionOptions) -> Session
Returns a new Session configured from the given SessionOptions. Fires /init in the background.

SessionOptions

publisherToken
String
required
Your unique publisher token.
userId
String
required
Stable identifier for the end user. Used for personalization, frequency capping, and rewarded ads.
conversationId
String
required
Unique ID of the current conversation / chat thread.
enabledPlacementCodes
[String]?
Placement codes to request ads for. Defaults to ["inlineAd"] when nil or empty.
character
Character?
AI character metadata for contextual targeting.
variantId
String?
Publisher-defined cohort identifier (e.g. for A/B testing).
regulatory
Regulatory?
Privacy / consent signals. TCF (gdpr / gdprConsent) is collected automatically if a TCF-compliant CMP is integrated — set manually only for COPPA, GPP, or US Privacy.
userEmail
String?
End-user email for frequency-cap deduplication.
advertisingId
String?
IDFA you collected yourself. Takes priority over the SDK’s automatic collection. Use with requestTrackingAuthorization: false.
vendorId
String?
IDFV you collected yourself. Falls back to UIDevice.current.identifierForVendor when nil.
requestTrackingAuthorization
Bool
Whether the SDK should auto-request ATT authorization. Defaults to true.
onEvent
(AdEvent) -> Void
Callback invoked on every ad lifecycle event. Called on the main thread.

Session

addMessage(_:options:)
method
Append a Message to the conversation. Synchronous. User messages trigger a debounced preload.
session.addMessage(Message(id: "m1", role: .user, content: "Hi"))
session.addMessage(
    Message(id: "m2", role: .user, content: "Hi"),
    options: AddMessageOptions(trackOnly: true)
)
When trackOnly: true, the preload is sent for analytics but bids are not processed.
createAd(_:options:)
method
Returns an Ad for the given messageId. Idempotent — repeated calls with the same messageId + placement code return the same Ad. Cache the result.
let ad = session.createAd("m2")
let sidebar = session.createAd("m2", options: AdOptions(code: "sidebar", theme: "dark"))
updateOptions(_:)
method
Live-update preload-scoped fields. See Live-updating session options.
destroy()
method
Tear down the session: cancel preloads, destroy ads, release web views. Idempotent.
eventPublisher
AnyPublisher<AdEvent, Never>
Combine publisher delivering the same events as onEvent. Useful for SwiftUI / Combine pipelines.
messages
[Message]
Read-only snapshot of messages tracked by the session.
sessionId
UUID?
Server-assigned session ID. nil until the first successful preload.
disabled
Bool
true if the server has permanently disabled the session via the /init response (e.g. geo-restriction). Subsequent preloads are skipped.
destroyed
Bool
true after destroy() is called.

MutablePublisherOptions

Subset of SessionOptions accepted by session.updateOptions(_:). Every field is optional — non-nil overwrites, nil leaves unchanged.
variantId
String?
regulatory
Regulatory?
userEmail
String?
advertisingId
String?
vendorId
String?

Message

id
String
required
Unique message ID.
role
Message.Role
required
.user or .assistant.
content
String
required
Message text.
createdAt
Date
Defaults to Date().

AdOptions

code
String
Placement code. Defaults to "inlineAd".
theme
String?
UI theme hint forwarded to the ad iframe (e.g. "dark").

AddMessageOptions

trackOnly
Bool
When true, the preload is still sent (for analytics) but bids are not processed — no ad is generated for this message. Defaults to false.

AdEvent

AdEvent is an enum with one typed payload per case. Every case has a stable string identifier accessible via event.name.

.filled(FilledData) — wire name ad.filled

A bid was filled for the placement.

.noFill(NoFillData) — wire name ad.no-fill

No ad was returned for the placement (server skipped).

.adHeight(AdHeightData) — wire name ad.height

The ad iframe reported a new height. Use to size the surrounding container (especially in UIKit UITableView / UICollectionView cells).

.viewed(ViewedData) — wire name ad.viewed

The ad was viewed by the user (IAB MRC viewability standard).

.clicked(ClickedData) — wire name ad.clicked

The user clicked the ad.

.renderStarted(RenderStartedData) — wire name ad.render-started

The first token of the ad content was received.

.renderCompleted(RenderCompletedData) — wire name ad.render-completed

Ad content streaming finished.

.error(ErrorData) — wire name ad.error

The SDK encountered an error while serving an ad.

.videoStarted(VideoStartedData) — wire name video.started

.videoCompleted(VideoCompletedData) — wire name video.completed

.rewardGranted(RewardGrantedData) — wire name reward.granted

Fired for rewarded-ad flows after the user qualifies for a reward.

Guides

Handling no-fill

Subscribe to .noFill to know when no ad was returned (geo-restriction, frequency cap, etc.). The skipCode payload tells you why.

Sizing ads in UIKit lists

Inside UITableView / UICollectionView cells, observe InlineAdUIView.onHeightChange and trigger a row resize when it fires. The height changes as the ad streams in and stabilizes at the final value. When the user updates their consent in your CMP, call:
session.updateOptions(MutablePublisherOptions(
    regulatory: Regulatory(gdpr: 1, gdprConsent: "<new-TCF-string>")
))
The next preload picks up the new value — no session recreation needed.

Switching character

The active character cannot be live-updated — the accumulated message history belongs to the original persona, so swapping mid-session would leave messages targeted at the wrong character. To switch character, destroy the current session and create a new one:
session.destroy()

session = KontextAds.createSession(SessionOptions(
    publisherToken: "<your-publisher-token>",
    userId: "user-1234",
    conversationId: "conv-new",
    character: newCharacter
))
The same applies to publisherToken, userId, conversationId, and enabledPlacementCodes — recreate the session whenever any of those change.

Loading older messages (conversation restore)

When restoring a conversation from your backend, call addMessage(_:) once per historical message in order. Preloads are debounced by 10ms, so rapid sequential calls coalesce into a single preload for the most recent user message — you won’t fire one preload per restored message.
for historicalMessage in loadedFromBackend {
    session.addMessage(Message(
        id: historicalMessage.id,
        role: historicalMessage.role,
        content: historicalMessage.content,
        createdAt: historicalMessage.timestamp
    ))
}

Pass every message, suppress ads with trackOnly

Always feed every message into the session, even when you don’t want an ad to appear (e.g. when a user is on a free trial, in a no-ads region, or you’ve decided to show ads only every Nth message). Skipping addMessage(_:) calls breaks the conversation context the server relies on for targeting. Use AddMessageOptions(trackOnly: true) to send the preload for analytics without generating a bid:
let shouldShowAd = userMessageCount.isMultiple(of: 5)

session.addMessage(
    Message(id: msg.id, role: .user, content: msg.content, createdAt: Date()),
    options: AddMessageOptions(trackOnly: !shouldShowAd)
)
When trackOnly: true, the preload still fires (server keeps full analytics) but no .filled event will arrive for that message and session.createAd(...) won’t resolve a bid.