` for the ad.
3. Call `session.createAd(messageId)` once the placeholder DOM exists, then `ad.mount(element)`.
4. When the message scrolls off-screen or the conversation ends, call `ad.destroy()` to release the iframe.
```ts theme={null}
const ad = session.createAd('msg-2', { theme: 'dark' })
ad.mount(document.getElementById('ad-msg-2'))
// later, when the message is removed:
ad.destroy()
```
If you'd rather do it in one call, `session.render({ messageId, element, theme })` is a convenience wrapper around `createAd + mount`.
## 4. Tear down
Call `session.destroy()` when the conversation ends or the page unmounts. Idempotent and required to cancel in-flight preloads and release iframe resources.
```ts theme={null}
session.destroy()
```
## Observing events
Subscribe to the lifecycle of every ad via the `onEvent` callback. Every event has a stable string `name` and a typed `payload`:
```ts theme={null}
const session = createSession({
// ...
onEvent: (event) => {
switch (event.name) {
case 'ad.filled':
console.log('filled:', event.payload.id, 'revenue=', event.payload.revenue)
break
case 'ad.clicked':
console.log('clicked:', event.payload.url)
break
case 'ad.no-fill':
console.log('no fill, skipCode=', event.payload.skipCode)
break
}
},
})
```
Every placement-attributed event (`ad.filled`, `ad.viewed`, `ad.clicked`, `ad.render-*`, `video.*`, `reward.granted`) also carries a top-level `code` field naming the matched placement, so publishers with multiple `enabledPlacementCodes` can disambiguate. Session-wide events (`ad.no-fill`, `ad.error`) omit it.
For SDK-internal diagnostics during integration, also pass an `onDebugEvent` callback — it receives `(name, data?)` for every internal step the SDK takes.
# Guides
Source: https://docs.kontext.so/sdk/js/guides
Patterns and recipes for common JavaScript SDK integration tasks.
## Handling no-fill
Subscribe to `ad.no-fill` to know when no ad was returned (geo-restriction, frequency cap, server-side skip). The `skipCode` payload tells you why.
```ts theme={null}
onEvent: ({ name, payload }) => {
if (name === 'ad.no-fill') {
console.log('No ad available:', payload.skipCode)
}
}
```
Every preload produces exactly one of `ad.filled` / `ad.no-fill` / `ad.error` per response, so you can rely on hearing back on every user message.
## 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. 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. See [Pacing](/concepts/pacing) for the full pattern.
Use `{ trackOnly: true }` to send the preload for analytics without generating an ad:
```ts theme={null}
const shouldShowAd = userMessageCount % 5 === 0
session.addMessage(
{ id: msg.id, role: 'user', content: msg.content, createdAt: new Date() },
{ trackOnly: !shouldShowAd }
)
```
When `trackOnly: true`, the preload still fires (server keeps full analytics) but no `ad.filled` event will arrive for that message and `session.createAd(...)` won't resolve to an ad.
## Live-updating consent mid-session
When the user updates their consent in your CMP, pass the new TCF string to `updateOptions`:
```ts theme={null}
session.updateOptions({
regulatory: { gdpr: 1, gdprConsent: '
' },
})
```
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:
```ts theme={null}
session.destroy()
session = createSession({
publisherToken: '',
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 (\~10 ms), so rapid sequential calls coalesce into a single preload for the most recent user message — you won't fire one preload per restored message.
```ts theme={null}
for (const m of loadedFromBackend) {
session.addMessage({
id: m.id,
role: m.role,
content: m.content,
createdAt: m.timestamp,
})
}
```
# Installation
Source: https://docs.kontext.so/sdk/js/installation
Install @kontextso/sdk-js via npm or load it from CDN.
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
## npm
```bash theme={null}
npm install @kontextso/sdk-js@4
```
## CDN
Or load the SDK from our CDN as a global script:
```html theme={null}
```
When loaded via CDN the API is available on `window.KontextSdk`. Replace `{publisherToken}` with your actual token.
# Kotlin SDK
Source: https://docs.kontext.so/sdk/kotlin
Integrate Kontext ads into your Android app with the Kotlin SDK.
See how easy it is to integrate high-performance ads into your Android app using our lightweight SDK.
## Requirements
* Android API 26+ (Android 8.0)
* Kotlin 1.9+
* Jetpack Compose for the recommended `InlineAd` integration (View interop is available via `InlineAdView` if your app is not on Compose)
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
Add the SDK via Gradle, configure GAID auto-collection.
Create a session, feed messages, mount an ad — the 5-minute integration.
Every type and method exposed by the SDK.
Patterns and recipes: no-fill, character switching, View interop, and more.
Run the bundled Compose demo end-to-end against your own publisher token.
# API reference
Source: https://docs.kontext.so/sdk/kotlin/api
Every type and method exposed by the Kotlin SDK.
## `KontextAds.createSession(...)`
```kotlin theme={null}
public fun createSession(context: Context, options: SessionOptions): Session
```
Returns a new `Session` configured from the given `SessionOptions`. Fires `/init` in the background. Pass `context.applicationContext` so the session is not tied to any single Activity.
## `SessionOptions`
Your unique publisher token.
Stable identifier for the end user. Used for personalization, frequency capping, and rewarded ads.
Unique ID of the current conversation / chat thread.
Placement codes to request ads for. Defaults to `["inlineAd"]` when null or empty.
Override for the ad-server base URL. Leave null to use Kontext's production endpoint.
AI character metadata for contextual targeting.
A `java.net.URI` — construct with `URI.create("…")`, not a raw `String`.
Publisher-defined cohort identifier (e.g. for A/B testing).
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.
0, 1, or null.
IAB TCF v2 consent string.
0, 1, or null.
Global Privacy Platform string.
GPP section IDs that apply.
CCPA / LSPA string.
End-user email for frequency-cap deduplication.
GAID you collected yourself. Takes priority over the SDK's automatic collection.
Callback invoked on every ad lifecycle event. Called on the main thread.
Optional diagnostic stream — fires for every internal SDK event (preload start/end, iframe lifecycle, geometry updates, etc.). Useful during integration. Leave null in production.
## `Session`
Append a `Message` to the conversation. Synchronous (not `suspend`). User messages trigger a debounced preload; assistant messages let the SDK link the matched ad to the placement.
```kotlin theme={null}
session.addMessage(Message(id = "m1", role = Role.USER, content = "Hi"))
session.addMessage(
Message(id = "m2", role = Role.USER, content = "Hi again"),
AddMessageOptions(trackOnly = true),
)
```
When `trackOnly = true`, the preload is sent for analytics but no ad is generated.
Returns an `Ad` for the given `messageId`. Idempotent — repeated calls with the same `messageId` + `code` + `theme` return the same `Ad`. Cache the result.
```kotlin theme={null}
val ad = session.createAd("m2")
val sidebar = session.createAd("m2", AdOptions(code = "sidebar", theme = "dark"))
```
Live-update preload-scoped fields. See [Guides → Live-updating session options](/sdk/kotlin/guides#live-updating-session-options).
Tear down the session: cancel preloads, destroy ads, release WebView resources. Idempotent. `close()` is an alias (implements `AutoCloseable`).
Hot Flow that delivers the same events as `onEvent`. Useful for ViewModels and coroutine pipelines.
Read-only snapshot of messages tracked by the session.
Server-assigned session ID. `null` until the first successful preload.
`true` if the server has permanently disabled the session via the `/init` response (e.g. geo-restriction). Subsequent preloads are skipped.
`true` after `destroy()` is called.
## `MutablePublisherOptions`
Subset of `SessionOptions` accepted by `session.updateOptions(...)`. Every field is optional — non-null overwrites, null leaves unchanged.
## `Message`
Unique message ID.
`Role.USER` or `Role.ASSISTANT`.
Message text.
Defaults to `Date()`.
## `AdOptions`
Placement code. Defaults to `"inlineAd"`.
UI theme hint forwarded to the ad iframe (e.g. `"dark"`).
## `AddMessageOptions`
When `true`, the preload is still sent (for analytics) but no ad is generated for this message. Defaults to `false`.
## UI
### `InlineAd` (Compose, recommended)
```kotlin theme={null}
@Composable
public fun InlineAd(
messageId: String,
session: Session,
code: String? = null,
theme: String? = null,
modifier: Modifier = Modifier,
)
```
Mounts the ad for the given `messageId`. Internally calls `session.createAd(...)`, attaches a pooled `WebView`, and reports container geometry back to the iframe. Safe to compose in a `LazyColumn` — the underlying `Ad` and `WebView` survive recomposition and scroll-off-screen recycling.
A second overload accepts a pre-resolved `Ad` directly:
```kotlin theme={null}
@Composable
public fun InlineAd(ad: Ad, modifier: Modifier = Modifier)
```
### `InlineAdView` (View interop)
```kotlin theme={null}
public class InlineAdView(context: Context) : FrameLayout {
public var onHeightChange: ((Float) -> Unit)?
public fun bind(messageId: String, session: Session, code: String? = null, theme: String? = null)
}
```
For apps not on Compose. Call `bind(messageId, session)` once after inflating; observe `onHeightChange` to resize the surrounding container (e.g. in a `RecyclerView` row).
## `AdEvent`
`AdEvent` is a sealed class with one typed payload per case. Every case has a stable string identifier accessible via `event.name`.
### `AdEvent.Filled` — wire name `ad.filled`
An ad was returned and linked to the placement.
Placement code this ad was matched to. Required for publishers with multiple `enabledPlacementCodes`.
### `AdEvent.NoFill` — wire name `ad.no-fill`
No ad was returned for the placement (server skipped).
Reason the ad was skipped.
### `AdEvent.AdHeight` — wire name `ad.height`
The ad iframe reported a new height. Use to size the surrounding container in `RecyclerView` rows. The `InlineAd` Compose composable handles this for you automatically.
### `AdEvent.Viewed` — wire name `ad.viewed`
The ad was viewed by the user (IAB MRC viewability standard).
### `AdEvent.Clicked` — wire name `ad.clicked`
The user clicked the ad.
Region of the ad that was clicked.
### `AdEvent.RenderStarted` — wire name `ad.render-started`
The first token of the ad content was received.
### `AdEvent.RenderCompleted` — wire name `ad.render-completed`
Ad content streaming finished.
### `AdEvent.Error` — wire name `ad.error`
The SDK encountered an error while serving an ad.
### `AdEvent.VideoStarted` — wire name `video.started`
### `AdEvent.VideoCompleted` — wire name `video.completed`
### `AdEvent.RewardGranted` — wire name `reward.granted`
Fired for rewarded-ad flows after the user qualifies for a reward.
# Example app
Source: https://docs.kontext.so/sdk/kotlin/example
Run the bundled Compose demo against your own publisher token.
The [`example`](https://github.com/kontextso/sdk-kotlin/tree/main/example) module in the SDK repo ships a working Jetpack Compose chat demo you can run end-to-end against your own publisher token.
## Clone and configure
```bash theme={null}
git clone https://github.com/kontextso/sdk-kotlin.git
cd sdk-kotlin
cp local.properties.example local.properties
# Edit local.properties and set `publisherToken` to your real token.
./gradlew :example:installDebug
```
Then launch the `Kontext v4 — Kotlin` app on a connected device or emulator.
`local.properties` is gitignored, so your token won't be committed. The example reads `publisherToken` (and an optional `adServerUrl` override) via `BuildConfig` at build time. If the file is missing, the build falls back to placeholder strings and `/preload` will fail until a real token is configured.
The default placeholder `publisherToken` won't return real ads — replace it with your token from the [publisher dashboard](/publishers#getting-started-is-easy).
## What you should see
After launch, the demo presents a chat interface with seeded user + assistant messages. As you send a message, the SDK fires `/preload` in the background and renders an ad below the assistant's reply.
The top app bar exposes a "Track only" toggle that flips `AddMessageOptions(trackOnly = true)` on outgoing messages — useful for verifying the `Kontextso-Is-Disabled` header behavior.
## Links
* [Kotlin SDK on GitHub](https://github.com/kontextso/sdk-kotlin)
* [Example app](https://github.com/kontextso/sdk-kotlin/tree/main/example)
* [Maven Central](https://repo1.maven.org/maven2/so/kontext/ads/)
* [Changelog](https://github.com/kontextso/sdk-kotlin/blob/main/CHANGELOG.md)
# Showing your first ad
Source: https://docs.kontext.so/sdk/kotlin/first-ad
Create a session, feed messages, mount InlineAd, and observe events.
This is the 5-minute integration walkthrough. Before you start, make sure you've completed [Installation](/sdk/kotlin/installation).
## 1. Create a session
The entry point is `KontextAds.createSession(context, options)`, which returns a `Session` you'll use for the rest of the conversation lifecycle.
```kotlin theme={null}
import java.net.URI
import so.kontext.ads.KontextAds
import so.kontext.ads.model.Character
import so.kontext.ads.model.SessionOptions
val session = KontextAds.createSession(
context = applicationContext,
options = SessionOptions(
publisherToken = "",
userId = "user-1234",
conversationId = "conv-5678",
character = Character(
id = "character-1234",
name = "John Doe",
avatarUrl = URI.create("https://example.com/avatar.png"),
greeting = "Hello, how can I help you today?",
),
onEvent = { event ->
// Handle ad lifecycle events
android.util.Log.d("kontext", "${event.name} $event")
},
),
)
```
The session keeps `publisherToken`, `userId`, and `conversationId` fixed for its lifetime. Recreate the session when any of those change (e.g. when the user starts a new chat).
Pass `context.applicationContext` rather than an Activity context — the Session outlives any single Activity and the SDK manages WebView resources internally.
## 2. 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 link the matched ad to the corresponding placement.
```kotlin theme={null}
import so.kontext.ads.model.Message
import so.kontext.ads.model.Role
session.addMessage(
Message(id = "msg-1", role = Role.USER, content = "Hello, how are you?"),
)
session.addMessage(
Message(id = "msg-2", role = Role.ASSISTANT, content = "I am good, thank you!"),
)
```
`addMessage(...)` returns synchronously — it is not a `suspend` function. The preload result is delivered later via the `onEvent` callback (`Filled`, `NoFill`, `Error`, …) — not via a return value.
## 3. Render the ad
The recommended way is the `InlineAd` composable. It takes the assistant message's `messageId` and the `Session`, and handles the rest:
```kotlin theme={null}
import androidx.compose.runtime.Composable
import so.kontext.ads.ui.InlineAd
@Composable
fun ChatRow(message: ChatMessage, session: Session) {
MessageBubble(message)
if (message.role == Role.ASSISTANT) {
InlineAd(messageId = message.id, session = session)
}
}
```
`InlineAd` is idempotent across recompositions — under the hood it calls `session.createAd(messageId)`, which returns the same `Ad` if one already exists for that `(messageId, code, theme)`. Scrolling the ad off-screen and back does **not** rebuild the iframe.
If your app is not on Compose, use `InlineAdView` instead — see [Guides → View interop](/sdk/kotlin/guides#view-interop-non-compose).
## 4. Tear down
Call `session.destroy()` when the conversation ends or the screen is closed. It is idempotent and required to cancel pending network requests and release WebView resources.
In Compose:
```kotlin theme={null}
DisposableEffect(session) {
onDispose { session.close() } // alias for session.destroy()
}
```
## Observing events
`AdEvent` is delivered both via the `onEvent` callback you pass to `SessionOptions` and via the `Session.events` Flow — pick whichever fits your codebase. Both deliver the same events on the main thread.
### `onEvent` callback
```kotlin theme={null}
val session = KontextAds.createSession(
context = applicationContext,
options = SessionOptions(
publisherToken = "...",
userId = "...",
conversationId = "...",
onEvent = { event ->
when (event) {
is AdEvent.Filled -> println("ad filled: ${event.bidId} revenue=${event.revenue}")
is AdEvent.Clicked -> println("clicked: ${event.url}")
else -> Unit
}
},
),
)
```
### Flow
```kotlin theme={null}
import kotlinx.coroutines.flow.collect
lifecycleScope.launch {
session.events.collect { event ->
println("event: ${event.name}")
}
}
```
The complete event list lives in the [API reference](/sdk/kotlin/api).
# Guides
Source: https://docs.kontext.so/sdk/kotlin/guides
Patterns and recipes for common Kotlin SDK integration tasks.
## Handling no-fill
Subscribe to `AdEvent.NoFill` to know when no ad was returned (geo-restriction, frequency cap, etc.). The `skipCode` payload tells you why.
```kotlin theme={null}
onEvent = { event ->
if (event is AdEvent.NoFill) {
android.util.Log.d("kontext", "no fill: ${event.skipCode}")
}
}
```
## Lifecycle management
Always call `session.destroy()` (or its alias `session.close()`) when the conversation ends or the screen is closed. It cancels in-flight preloads, tears down every mounted ad, finalizes the OMID session, and releases WebView resources. Idempotent.
In Compose, anchor it to the screen-level `DisposableEffect`:
```kotlin theme={null}
DisposableEffect(session) {
onDispose { session.close() }
}
```
In a ViewModel:
```kotlin theme={null}
override fun onCleared() {
session.destroy()
super.onCleared()
}
```
## Live-updating session options
A subset of session options can be live-updated without recreating the session. Updates are read on the next `/preload`, so changes take effect on the next user message.
```kotlin theme={null}
import so.kontext.ads.model.MutablePublisherOptions
session.updateOptions(MutablePublisherOptions(variantId = "new-variant"))
```
Live-updateable fields: `variantId`, `regulatory`, `userEmail`, `advertisingId`. Non-null fields overwrite; null 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 the accumulated message history targeted at the wrong persona. Recreate the session instead.
## Live-updating consent mid-session
When the user updates their consent in your CMP, call:
```kotlin theme={null}
import so.kontext.ads.model.Regulatory
session.updateOptions(MutablePublisherOptions(
regulatory = Regulatory(gdpr = 1, gdprConsent = ""),
))
```
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:
```kotlin theme={null}
session.destroy()
session = KontextAds.createSession(
context = applicationContext,
options = SessionOptions(
publisherToken = "",
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, so rapid sequential calls coalesce into a single preload for the most recent user message — you won't fire one preload per restored message.
```kotlin theme={null}
for (historical in loadedFromBackend) {
session.addMessage(
Message(
id = historical.id,
role = historical.role,
content = historical.content,
),
)
}
```
## 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 paid tier, 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. See [Pacing](/concepts/pacing) for the full pattern.
```kotlin theme={null}
import so.kontext.ads.model.AddMessageOptions
val shouldShowAd = userMessageCount % 5 == 0
session.addMessage(
Message(id = msg.id, role = Role.USER, content = msg.content),
AddMessageOptions(trackOnly = !shouldShowAd),
)
```
When `trackOnly = true`, the preload still fires (server keeps full analytics) but no `AdEvent.Filled` will arrive for that message and `session.createAd(...)` won't resolve to a fillable ad.
## Theming
Pass a publisher-defined theme string to the `InlineAd` composable; it propagates into the iframe's `update-iframe` payload and is available to ad creatives.
```kotlin theme={null}
InlineAd(
messageId = msg.id,
session = session,
theme = if (isSystemInDarkTheme()) "dark" else "light",
)
```
Theme is part of the `Ad` identity key — flipping it for the same `messageId` causes the iframe to reload with the new value.
## View interop (non-Compose)
For apps not on Jetpack Compose, use `InlineAdView` (a `FrameLayout` subclass). Inflate or instantiate it like any other view, then call `bind(messageId, session)`:
```kotlin theme={null}
import so.kontext.ads.ui.InlineAdView
val adView = InlineAdView(context)
adView.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
adView.onHeightChange = { newHeight ->
// Resize the surrounding row if you're inside a RecyclerView
requestRowLayout()
}
adView.bind(messageId = assistantMessageId, session = session)
container.addView(adView)
```
The view follows the same lifecycle rules as the composable — call `session.destroy()` on screen teardown; the view's `WebView` is recycled by the session's pool.
# Installation
Source: https://docs.kontext.so/sdk/kotlin/installation
Add the Kotlin SDK to your Android project and configure GAID.
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
## 1. Add the SDK
The SDK is published to Maven Central as `so.kontext:ads`. Make sure `mavenCentral()` is listed in your project's repository configuration:
```kotlin theme={null}
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
```
Then add the dependency to your app-level `build.gradle.kts`:
```kotlin theme={null}
dependencies {
implementation("so.kontext:ads:4.0.3") // replace with the latest 4.x
}
```
The SDK targets Android API 26+ and is JVM 17. If your app sets a lower target/JVM, bump those before integrating.
## 2. Google Advertising ID (GAID)
The SDK automatically reads and forwards the Google Advertising ID with each ad request.
The `com.google.android.gms.permission.AD_ID` permission is automatically merged into your `AndroidManifest.xml` via the SDK's manifest contribution — no changes to your manifest are required. GAID is an install-time permission, so there is no runtime prompt.
If your app targets Android 13+ (API 33+) and you have explicitly set `tools:node="remove"` on the `AD_ID` permission anywhere in your manifest, remove that override or the advertising identifier will not be accessible at runtime.
If you collect GAID yourself, you can pass it directly via `SessionOptions.advertisingId` — that value takes priority over the SDK's automatic collection.
# React SDK
Source: https://docs.kontext.so/sdk/react
Integrate Kontext ads into your React app with the React SDK.
See how easy it is to integrate high-performance ads into your React app using our lightweight SDK.
## Requirements
* **React**: 18.0.0 or later
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
Install the npm package and prepare your app.
Wrap your app in ``, feed messages, mount `` — the 5-minute integration.
Props, hooks, components, and events.
Patterns and recipes: no-fill, character switching, restoring history, and more.
Run the Vite + React demo end-to-end against your own publisher token.
# React Native SDK
Source: https://docs.kontext.so/sdk/react-native
Get started with our SDK built for iOS and Android apps using React Native
We are preparing a new v4 release of the React Native SDK. The documentation below covers the current shipping version — once v4 lands, this page will be updated.
See how easy it is to integrate high-performance ads into your React Native app using our lightweight SDK.
## Requirements
* **React Native**: 0.73.2 or later
## Getting started
### 1. Installation
To get started, you will need to set up a [publisher account](/publishers#getting-started-is-easy) to get a `publisherToken` and `code`.
```bash theme={null}
# Install the SDK
npm install @kontextso/sdk-react-native
# Install required peer dependencies
npm install react-native-device-info react-native-webview @react-native-community/netinfo
```
### 2. Initialize AdsProvider
The `AdsProvider` handles all data-fetching logic and must have access to the chat messages. Place it high enough in your component tree so it can contain all ad placements.
```tsx theme={null}
import * as React from 'react'
import { useState } from 'react'
import { View } from 'react-native'
import { AdsProvider } from '@kontextso/sdk-react-native'
interface Message {
id: string
role: 'user' | 'assistant'
content: string
createdAt: Date
}
function App() {
const [messages, setMessages] = useState([])
return (
{
// process events
}}
>
)
}
```
### 3. Set up IFA (Identifier for Advertisers)
* **iOS:** add `NSUserTrackingUsageDescription` to `ios//Info.plist`. The SDK auto-prompts for ATT and reads IDFA from there.
* **Android:** the SDK adds the `com.google.android.gms.permission.AD_ID` permission via manifest merger automatically — no changes to your `AndroidManifest.xml` required.
See [IFA & ATT](/guides/ifa) for the full setup — required keys, prompt-timing gotchas, and how to manage the prompt yourself.
### 4. Set up SKAdNetwork (iOS only)
Add the SKAdNetwork identifiers we provide during onboarding to `ios//Info.plist`. The SDK reads them and forwards them on every `/init` so DSPs can measure conversions.
See [SKAdNetwork](/guides/skadnetwork) for the full guide.
### 5. Show your first ad
An ad slot is a designated area in your UI where an ad can be rendered. In most cases, it appears below a chat message. During onboarding, you’ll receive a unique `code` for each ad slot you plan to use.
Copy the markup `` and place it in your application where it should be rendered. Don't forget to assign `messageId` as a unique identifier. For example, if you have a `MessageList` component, you can show an ad after each message like this (every message will have a unique ad displayed because of `messageId`).
```tsx theme={null}
function MessageList({ messages }: { messages: Message[] }) {
return (
{messages.map((m) => (
))}
)
}
```
## API documentation
### `AdsProvider` properties
Your unique publisher token.
List of messages between the assistant and the user.
Unique ID of the message.
Role of the message (`user` or `assistant`).
Message text.
Timestamp when the message was created.
Unique identifier that remains the same for the user’s lifetime (used for retargeting and rewarded ads).
Email of the user.
Unique ID of the conversation.
Placement codes enabled for the conversation. Example: `['inlineAd']`.
Character object used in this conversation.
Unique ID of the character.
Name of the character.
URL of the character’s avatar.
Indicates whether the character is NSFW.
The character’s greeting.
Description of the character’s personality.
Tags describing the character.
Regulatory compliance information.
**TCF (Transparency and Consent Framework)** signals — `gdpr` and `gdprConsent` — are handled automatically by the SDK if you have a TCF-compliant CMP (Consent Management Platform) integrated in your app. You do not need to set these manually.
Indicates whether the request is subject to GDPR regulations. (0 = No, 1 = Yes, null = Unknown)
The IAB Transparency and Consent Framework (TCF) consent string.
Indicates whether the request is subject to COPPA. (0 = No, 1 = Yes, null = Unknown)
Global Privacy Platform (GPP) consent string.
IDs of the GPP sections that apply to this transaction.
Signals regarding consumer privacy under US privacy regulations (e.g., CCPA, LSPA).
Publisher-provided identifier for the user cohort (for A/B testing).
Callback triggered when an event occurs. See [Supported events](#supported-events) for more details.
Flag indicating if the ads are disabled.
Note: This does not disable the display of old ads; that behavior is controlled by `staleAdsHandling`.
Determines how stale ads (ads not linked to the latest message) are handled.
* `preserve` - keep displaying the last anchored ad until it's replaced by a new ad
* `hide` (default) - hide the ad when it becomes stale
### `InlineAd` properties
Placement code provided during onboarding.
Unique ID of the message.
Theme of the ad, e.g. `light` or `dark`.
Wrapper function to wrap the ad content.
### Supported Events
#### `ad.clicked`
The user has clicked the ad.
Ad ID.
Generated ad content.
Message ID.
Click URL.
Ad format.
#### `ad.viewed`
The user has viewed the ad.
Ad ID.
Generated ad content.
Message ID.
Ad format.
The revenue of the ad (eCPM in USD).
#### `ad.filled`
Ad is available.
The revenue of the ad (eCPM in USD).
#### `ad.no-fill`
Ad is not available.
The code indicating the reason why the ad was skipped.
#### `ad.render-started`
Triggered before the first token is received.
Ad ID.
#### `ad.render-completed`
Triggered after the last token is received.
Ad ID.
#### `ad.error`
Triggered when an error occurs.
Error message.
Error code.
#### `reward.granted`
Triggered when the user receives a reward.
Ad ID.
#### `video.started`
Triggered when the video playback starts.
Ad ID.
#### `video.completed`
Triggered when the video playback finishes.
Ad ID.
## Guides
### Handling no-fill events
You can notify when the ad is not available by using the `onEvent` callback.
```tsx theme={null}
{
if (name === 'ad.no-fill') {
console.log('Ad is not available');
}
}}
/>
```
## Links
* [NPM Package](https://www.npmjs.com/package/@kontextso/sdk-react-native)
* [React Native Demo Project](https://github.com/kontextso/sdk-react-native-demo)
# API reference
Source: https://docs.kontext.so/sdk/react/api
Props, hooks, components, and events exposed by the React SDK.
## `` props
Your unique publisher token.
Unique identifier that remains the same for the user's lifetime (used for retargeting and rewarded ads).
Email of the user.
Unique ID of the conversation.
Placement codes enabled for the conversation. Defaults to `['inlineAd']`.
Character object used in this conversation.
Unique ID of the character.
Name of the character.
URL of the character's avatar.
Indicates whether the character is NSFW.
The character's greeting.
Description of the character's personality.
Tags describing the character.
Regulatory object used in this conversation.
Indicates whether the request is subject to GDPR regulations. (0 = No, 1 = Yes, null = Unknown)
The IAB Transparency and Consent Framework (TCF) consent string.
Indicates whether the request is subject to COPPA. (0 = No, 1 = Yes, null = Unknown)
Global Privacy Platform (GPP) consent string.
IDs of the GPP sections that apply to this transaction.
Signals regarding consumer privacy under US privacy regulations (e.g., CCPA, LSPA).
Publisher-provided identifier for the user cohort (for A/B testing).
Callback triggered when an event occurs. See [Supported events](#supported-events) below.
Callback for SDK-internal diagnostic events. Receives `(name, data?)` — useful when investigating preload/session behaviour during integration. Off by default.
## `useAds()` hook
Returns `addMessage`, a forwarder that always routes to the current session. If `` swaps the session (e.g. `publisherToken` change), the next call hits the new one automatically.
`(message: Message, options?: AddMessageOptions) => void` — register a new chat message with the SDK. The next preload is scheduled automatically.
`AddMessageOptions` currently exposes one flag:
When `true`, the preload request still goes out for analytics but no ad will be shown for this message. Use this to suppress ads without losing tracking data.
## `Message` shape
Unique ID of the message.
Role of the message (`user` or `assistant`).
Message text.
Timestamp when the message was created.
## `` props
Unique ID of the message this ad belongs to.
Placement code provided during onboarding. Defaults to `'inlineAd'`.
Theme of the ad, e.g. `light` or `dark`.
Render-prop function that lets you wrap the ad iframe in your own markup. The function receives the rendered ad as a React node:
```tsx theme={null}
{ad}
}
/>
```
## ``
`` is wrapped in an internal `ErrorBoundary` so a broken ad never crashes the host app. The component is also exported in case you want to wrap your own logic:
```tsx theme={null}
import { ErrorBoundary } from '@kontextso/sdk-react'
```
## Supported events
### `ad.clicked`
The user has clicked the ad.
Ad ID.
Generated ad content.
Message ID.
Click URL.
Ad format.
Region of the creative the user clicked (e.g. `'cta'`, `'banner'`).
### `ad.viewed`
The user has viewed the ad.
Ad ID.
Generated ad content.
Message ID.
Ad format.
The revenue of the ad (eCPM in USD).
### `ad.filled`
Ad is available.
Ad ID.
Placement code this ad was matched to.
The revenue of the ad (eCPM in USD).
### `ad.no-fill`
Ad is not available.
The code indicating the reason why the ad was skipped.
### `ad.render-started`
Triggered before the first token is received.
Ad ID.
### `ad.render-completed`
Triggered after the last token is received.
Ad ID.
### `ad.error`
Triggered when an error occurs.
Error message.
Error code.
### `video.started`
Triggered when the video playback starts.
Ad ID.
### `video.completed`
Triggered when the video playback finishes.
Ad ID.
### `reward.granted`
Triggered when a rewarded-ad reward is granted to the user.
Ad ID.
# Example app
Source: https://docs.kontext.so/sdk/react/example
Run the Next.js demo against your own publisher token.
The [`sdk-react-demo`](https://github.com/kontextso/sdk-react-demo) repo ships a minimal Next.js app you can run end-to-end against your own publisher token.
## Clone and configure
```bash theme={null}
git clone https://github.com/kontextso/sdk-react-demo.git
cd sdk-react-demo
npm install
# Edit src/app/constants.ts and set PUBLISHER_TOKEN + PLACEMENT_CODE.
npm run dev
```
The repo ships placeholder constants — replace `PUBLISHER_TOKEN` with your token from the [publisher dashboard](/publishers#getting-started-is-easy) and set `PLACEMENT_CODE` to a placement enabled for your publisher (typically `inlineAd`).
## What you should see
After `npm run dev`, open the printed local URL in a browser. The demo presents a chat interface with seeded user + assistant messages. As you send a message, the SDK fires `/preload` in the background and renders an ad below the assistant's reply.
## Links
* [NPM Package](https://www.npmjs.com/package/@kontextso/sdk-react)
* [React Demo Project](https://github.com/kontextso/sdk-react-demo)
# Showing your first ad
Source: https://docs.kontext.so/sdk/react/first-ad
Wrap your app in , feed messages, mount , observe events.
This is the 5-minute integration walkthrough. Before you start, make sure you've completed [Installation](/sdk/react/installation).
## 1. Wrap your app in ``
`` initializes a session that's available to every component inside it via the `useAds()` hook. Place it high enough in your tree so it can contain all ad placements.
```tsx theme={null}
import { AdsProvider } from '@kontextso/sdk-react'
function App() {
return (
{
// process events
}}
>
)
}
```
## 2. Feed messages to the SDK
The SDK preloads ads in response to new chat messages. Call `addMessage()` from the `useAds()` hook whenever a user or assistant message is created.
```tsx theme={null}
import { useAds } from '@kontextso/sdk-react'
const { addMessage } = useAds()
// Call this on every user and assistant message in the conversation.
addMessage({
id: '',
role: '',
content: '',
createdAt: new Date(),
})
```
## 3. Mount ``
An ad slot is a designated area in your UI where an ad can be rendered. In most cases, it appears below an assistant message.
Place `` wherever the ad should appear and pass the `messageId` of the assistant message it's associated with.
```tsx theme={null}
import { InlineAd } from '@kontextso/sdk-react'
function MessageList({ messages }: { messages: Message[] }) {
return (
{messages.map((m) => (
{m.role === 'assistant' && }
))}
)
}
```
## Observing events
Subscribe to ad lifecycle events via the `` `onEvent` prop. Every event has a stable `name` and a typed `payload`:
```tsx theme={null}
{
switch (event.name) {
case 'ad.filled':
console.log('filled:', event.payload.id, 'revenue=', event.payload.revenue)
break
case 'ad.clicked':
console.log('clicked:', event.payload.url)
break
case 'ad.no-fill':
console.log('no fill, skipCode=', event.payload.skipCode)
break
}
}}
>
```
Every placement-attributed event (`ad.filled`, `ad.viewed`, `ad.clicked`, `ad.render-*`, `video.*`, `reward.granted`) also carries a top-level `code` field naming the matched placement, so publishers with multiple `enabledPlacementCodes` can disambiguate. Session-wide events (`ad.no-fill`, `ad.error`) omit it.
For SDK-internal diagnostics during integration, also pass an `onDebugEvent` callback — it receives `(name, data?)` for every internal step.
# Guides
Source: https://docs.kontext.so/sdk/react/guides
Patterns and recipes for common React SDK integration tasks.
## Handling no-fill
Subscribe to `ad.no-fill` to know when no ad was returned (geo-restriction, frequency cap, server-side skip). The `skipCode` payload tells you why.
```tsx theme={null}
{
if (name === 'ad.no-fill') {
console.log('No ad available:', payload.skipCode)
}
}}
>
```
Every preload produces exactly one of `ad.filled` / `ad.no-fill` / `ad.error` per response, so you can rely on hearing back on every user message.
## 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. 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. See [Pacing](/concepts/pacing) for the full pattern.
Use `{ trackOnly: true }` to send the preload for analytics without generating an ad:
```tsx theme={null}
const { addMessage } = useAds()
const shouldShowAd = userMessageCount % 5 === 0
addMessage(
{ id: msg.id, role: 'user', content: msg.content, createdAt: new Date() },
{ trackOnly: !shouldShowAd }
)
```
When `trackOnly: true`, the preload still fires (server keeps full analytics) but no `ad.filled` event will arrive for that message and no `` will render content.
## Live-updating consent mid-session
`` reactively passes new props into the underlying session. Bind `regulatory` to your CMP state and the next preload picks up the new value automatically:
```tsx theme={null}
```
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, change the `conversationId` (and `character`) prop on ``. The provider tears down the previous session and spins up a new one automatically:
```tsx theme={null}
```
The same applies to `publisherToken`, `userId`, `conversationId`, and `enabledPlacementCodes` — changing any of those rebuilds the session.
## Loading older messages (conversation restore)
When restoring a conversation from your backend, call `addMessage(...)` once per historical message in order. Preloads are debounced (\~10 ms), so rapid sequential calls coalesce into a single preload for the most recent user message — you won't fire one preload per restored message.
```tsx theme={null}
const { addMessage } = useAds()
useEffect(() => {
for (const m of loadedFromBackend) {
addMessage({
id: m.id,
role: m.role,
content: m.content,
createdAt: m.timestamp,
})
}
}, [loadedFromBackend])
```
# Installation
Source: https://docs.kontext.so/sdk/react/installation
Install @kontextso/sdk-react.
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
```bash theme={null}
npm install @kontextso/sdk-react@4
```
That's it — there are no peer dependencies or platform-specific config to add. Head to [Showing your first ad](/sdk/react/first-ad) to wire it up.
# Swift SDK
Source: https://docs.kontext.so/sdk/swift
Integrate Kontext ads into your iOS app with the 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+
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
Add the SDK via SPM or CocoaPods, configure ATT and SKAdNetwork.
Create a session, feed messages, mount an ad — the 5-minute integration.
Every type and method exposed by the SDK.
Patterns and recipes: no-fill, character switching, SwiftUI wrapping, and more.
Run the bundled UIKit demo end-to-end against your own publisher token.
# API reference
Source: https://docs.kontext.so/sdk/swift/api
Every type and method exposed by the Swift SDK.
## `KontextAds.createSession(_:)`
```swift theme={null}
@MainActor
public static func createSession(_ options: SessionOptions) -> Session
```
Returns a new `Session` configured from the given `SessionOptions`. Fires `/init` in the background.
## `SessionOptions`
Your unique publisher token.
Stable identifier for the end user. Used for personalization, frequency capping, and rewarded ads.
Unique ID of the current conversation / chat thread.
Placement codes to request ads for. Defaults to `["inlineAd"]` when nil or empty.
AI character metadata for contextual targeting.
Publisher-defined cohort identifier (e.g. for A/B testing).
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.
0, 1, or nil.
IAB TCF v2 consent string.
0, 1, or nil.
Global Privacy Platform string.
GPP section IDs that apply.
CCPA / LSPA string.
End-user email for frequency-cap deduplication.
IDFA you collected yourself. Takes priority over the SDK's automatic collection. Use with `requestTrackingAuthorization: false`.
IDFV you collected yourself. Falls back to `UIDevice.current.identifierForVendor` when nil.
Whether the SDK should auto-request ATT authorization. Defaults to `true`.
Callback invoked on every ad lifecycle event. Called on the main thread.
## `Session`
Append a `Message` to the conversation. Synchronous. User messages trigger a debounced preload.
```swift theme={null}
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 no ad is generated.
Returns an `Ad` for the given `messageId`. Idempotent — repeated calls with the same `messageId` + placement code return the same `Ad`. Cache the result.
```swift theme={null}
let ad = session.createAd("m2")
let sidebar = session.createAd("m2", options: AdOptions(code: "sidebar", theme: "dark"))
```
Live-update preload-scoped fields. See [Guides → Live-updating session options](/sdk/swift/guides#live-updating-session-options).
Tear down the session: cancel preloads, destroy ads, release web views. Idempotent.
Combine publisher delivering the same events as `onEvent`. Useful for SwiftUI / Combine pipelines.
Read-only snapshot of messages tracked by the session.
Server-assigned session ID. `nil` until the first successful preload.
`true` if the server has permanently disabled the session via the `/init` response (e.g. geo-restriction). Subsequent preloads are skipped.
`true` after `destroy()` is called.
## `MutablePublisherOptions`
Subset of `SessionOptions` accepted by `session.updateOptions(_:)`. Every field is optional — non-nil overwrites, nil leaves unchanged.
## `Message`
Unique message ID.
`.user` or `.assistant`.
Message text.
Defaults to `Date()`.
## `AdOptions`
Placement code. Defaults to `"inlineAd"`.
UI theme hint forwarded to the ad iframe (e.g. `"dark"`).
## `AddMessageOptions`
When `true`, the preload is still sent (for analytics) but 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`
An ad was returned and linked to the placement.
Placement code this ad was matched to. Required for publishers with multiple `enabledPlacementCodes`.
### `.noFill(NoFillData)` — wire name `ad.no-fill`
No ad was returned for the placement (server skipped).
Reason the ad was 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.
Region of the ad that was clicked.
### `.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.
# Example app
Source: https://docs.kontext.so/sdk/swift/example
Run the bundled UIKit demo against your own publisher token.
The [`Example`](https://github.com/kontextso/sdk-swift/tree/main/Example) directory in the SDK repo ships a working UIKit demo you can run end-to-end against your own publisher token.
## Clone and configure
```bash theme={null}
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](/publishers#getting-started-is-easy).
## What you should see
After launch, the demo presents a chat interface with seeded user + assistant messages. As you send a message, the SDK fires `/preload` in the background and renders an ad below the assistant's reply.
## Links
* [Swift SDK on GitHub](https://github.com/kontextso/sdk-swift)
* [UIKit example app](https://github.com/kontextso/sdk-swift/tree/main/Example)
* [KontextKit (shared iOS primitives)](https://github.com/kontextso/kontextkit-ios)
# Showing your first ad
Source: https://docs.kontext.so/sdk/swift/first-ad
Create a session, feed messages, mount InlineAdUIView, and observe events.
This is the 5-minute integration walkthrough. Before you start, make sure you've completed [Installation](/sdk/swift/installation).
## 1. Create a session
The entry point is `KontextAds.createSession(_:)`, which returns a `Session` you'll use for the rest of the conversation lifecycle.
```swift theme={null}
import KontextSwiftSDK
let session = KontextAds.createSession(SessionOptions(
publisherToken: "",
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).
## 2. 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 link the matched ad to the corresponding placement.
```swift theme={null}
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.
## 3. 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.
```swift theme={null}
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)
}
```
## 4. 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.
```swift theme={null}
session.destroy()
```
## Observing events
Two equivalent ways to consume `AdEvent`s — pick whichever fits your codebase.
### `onEvent` callback
```swift theme={null}
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
```swift theme={null}
import Combine
var cancellables: Set = []
session.eventPublisher
.sink { event in
print("event: \(event.name)")
}
.store(in: &cancellables)
```
Both deliver the same events on the main thread. The complete event list lives in the [API reference](/sdk/swift/api).
# Guides
Source: https://docs.kontext.so/sdk/swift/guides
Patterns and recipes for common Swift SDK integration tasks.
## Handling no-fill
Subscribe to `.noFill` to know when no ad was returned (geo-restriction, frequency cap, etc.). The `skipCode` payload tells you why.
```swift theme={null}
onEvent: { event in
if case .noFill(let data) = event {
print("no fill:", data.skipCode)
}
}
```
## 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.
```swift theme={null}
let adView = InlineAdUIView(ad: ad)
adView.onHeightChange = { [weak self] height in
self?.updateRowHeight(height)
}
```
## Live-updating session options
A subset of session options can be live-updated without recreating the session. Updates are read on the next `/preload`, so changes take effect on the next user message.
```swift theme={null}
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.
## Live-updating consent mid-session
When the user updates their consent in your CMP, call:
```swift theme={null}
session.updateOptions(MutablePublisherOptions(
regulatory: Regulatory(gdpr: 1, gdprConsent: "")
))
```
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:
```swift theme={null}
session.destroy()
session = KontextAds.createSession(SessionOptions(
publisherToken: "",
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 10 ms, so rapid sequential calls coalesce into a single preload for the most recent user message — you won't fire one preload per restored message.
```swift theme={null}
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. See [Pacing](/concepts/pacing) for the full pattern.
```swift theme={null}
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 to an ad.
## SwiftUI
The SDK ships UIKit views only. Wrap `InlineAdUIView` in a `UIViewRepresentable` to embed it in SwiftUI:
```swift theme={null}
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) {}
}
```
# Installation
Source: https://docs.kontext.so/sdk/swift/installation
Add the Swift SDK to your iOS project and configure ATT + SKAdNetwork.
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
## 1. Add the SDK
Add the SDK as a package dependency:
```swift theme={null}
dependencies: [
.package(url: "https://github.com/kontextso/sdk-swift", .upToNextMajor(from: "4.0.0")) // pulls the latest 4.x
]
```
Then add the `KontextSwiftSDK` product to your target.
Add to your `Podfile`:
```ruby theme={null}
pod 'KontextSwiftSDK', '~> 4.0' # pulls the latest 4.x
```
Then run `pod install`.
## 2. Set up IDFA (App Tracking Transparency)
Add `NSUserTrackingUsageDescription` to `Info.plist`. The SDK auto-prompts for ATT the first time a session is created and reads IDFA + IDFV from there.
See [IFA & ATT](/guides/ifa) for the full setup — required `Info.plist` key, prompt-timing gotchas, and how to manage the prompt yourself.
## 3. Set up SKAdNetwork
Add Kontext's own ad network identifier — plus the DSP identifiers we provide during onboarding — to `Info.plist`. The SDK reads them at startup and forwards them on every `/init`.
```xml theme={null}
SKAdNetworkItems
SKAdNetworkIdentifier
mp7rpxwdrx.skadnetwork
```
Without Kontext's identifier, installs from directly-served campaigns silently won't be attributed. See [SKAdNetwork](/guides/skadnetwork) for the full guide.
# Vue SDK
Source: https://docs.kontext.so/sdk/vue
Integrate Kontext ads into your Vue 3 app with the Vue SDK.
See how easy it is to integrate high-performance ads into your Vue app using our lightweight SDK.
## Requirements
* **Vue**: 3.3.0 or later
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
Install the npm package.
Install the plugin, feed messages, mount `` — the 5-minute integration.
Plugin options, composable, components, and events.
Patterns and recipes: no-fill, character switching, restoring history, and more.
Run the Vite + Vue 3 demo end-to-end against your own publisher token.
# API reference
Source: https://docs.kontext.so/sdk/vue/api
Plugin options, composable, components, and events exposed by the Vue SDK.
## Plugin / `AdsProvider` options
Your unique publisher token.
Unique identifier that remains the same for the user's lifetime (used for retargeting and rewarded ads).
Email of the user.
Unique ID of the conversation.
Placement codes enabled for the conversation. Defaults to `['inlineAd']`.
Character object used in this conversation.
Unique ID of the character.
Name of the character.
URL of the character's avatar.
Indicates whether the character is NSFW.
The character's greeting.
Description of the character's personality.
Tags describing the character.
Regulatory object used in this conversation.
Indicates whether the request is subject to GDPR regulations. (0 = No, 1 = Yes, null = Unknown)
The IAB Transparency and Consent Framework (TCF) consent string.
Indicates whether the request is subject to COPPA. (0 = No, 1 = Yes, null = Unknown)
Global Privacy Platform (GPP) consent string.
IDs of the GPP sections that apply to this transaction.
Signals regarding consumer privacy under US privacy regulations (e.g., CCPA, LSPA).
Publisher-provided identifier for the user cohort (for A/B testing).
Callback triggered when an event occurs. See [Supported events](#supported-events) below.
Callback for SDK-internal diagnostic events. Receives `(name, data?)` — useful when investigating preload/session behaviour during integration. Off by default.
## `useAds()` composable
Returns `addMessage`, a forwarder that always routes to the current session. If `` swaps the session (e.g. `publisherToken` change), the next call hits the new one automatically.
`(message: Message, options?: AddMessageOptions) => void` — register a new chat message with the SDK. The next preload is scheduled automatically.
`AddMessageOptions` currently exposes one flag:
When `true`, the preload request still goes out for analytics but no ad will be shown for this message. Use this to suppress ads without losing tracking data.
## `Message` shape
Unique ID of the message.
Role of the message (`user` or `assistant`).
Message text.
Timestamp when the message was created.
## `InlineAd` properties
Unique ID of the message this ad belongs to.
Placement code provided during onboarding. Defaults to `'inlineAd'`.
Theme of the ad, e.g. `light` or `dark`.
### Slots
Render-prop slot that lets you wrap the ad iframe in your own markup. The slot receives the rendered ad as `ad`:
```vue theme={null}
{{ ad }}
```
## Supported events
### `ad.clicked`
The user has clicked the ad.
Ad ID.
Generated ad content.
Message ID.
Click URL.
Ad format.
Region of the creative the user clicked (e.g. `'cta'`, `'banner'`).
### `ad.viewed`
The user has viewed the ad.
Ad ID.
Generated ad content.
Message ID.
Ad format.
The revenue of the ad (eCPM in USD).
### `ad.filled`
Ad is available.
Ad ID.
Placement code this ad was matched to.
The revenue of the ad (eCPM in USD).
### `ad.no-fill`
Ad is not available.
The code indicating the reason why the ad was skipped.
### `ad.render-started`
Triggered before the first token is received.
Ad ID.
### `ad.render-completed`
Triggered after the last token is received.
Ad ID.
### `ad.error`
Triggered when an error occurs.
Error message.
Error code.
### `video.started`
Triggered when the video playback starts.
Ad ID.
### `video.completed`
Triggered when the video playback finishes.
Ad ID.
### `reward.granted`
Triggered when a rewarded-ad reward is granted to the user.
Ad ID.
# Example app
Source: https://docs.kontext.so/sdk/vue/example
Run the Vite + Vue 3 demo against your own publisher token.
The [`sdk-vue-demo`](https://github.com/kontextso/sdk-vue-demo) repo ships a minimal Vite + Vue 3 app you can run end-to-end against your own publisher token.
## Clone and configure
```bash theme={null}
git clone https://github.com/kontextso/sdk-vue-demo.git
cd sdk-vue-demo
npm install
# Edit src/constants.ts and set PUBLISHER_TOKEN + PLACEMENT_CODE.
npm run dev
```
The repo ships placeholder constants — replace `PUBLISHER_TOKEN` with your token from the [publisher dashboard](/publishers#getting-started-is-easy) and set `PLACEMENT_CODE` to a placement enabled for your publisher (typically `inlineAd`).
## What you should see
After `npm run dev`, open the printed local URL in a browser. The demo presents a chat interface with seeded user + assistant messages. As you send a message, the SDK fires `/preload` in the background and renders an ad below the assistant's reply.
## Links
* [NPM Package](https://www.npmjs.com/package/@kontextso/sdk-vue)
* [Vue Demo Project](https://github.com/kontextso/sdk-vue-demo)
# Showing your first ad
Source: https://docs.kontext.so/sdk/vue/first-ad
Install the Kontext plugin, feed messages, mount , observe events.
This is the 5-minute integration walkthrough. Before you start, make sure you've completed [Installation](/sdk/vue/installation).
## 1. Install the plugin
The `KontextAdsPlugin` initializes a session that's available to every component in your app via the `useAds()` composable.
```ts theme={null}
// main.ts
import { createApp } from 'vue'
import { KontextAdsPlugin } from '@kontextso/sdk-vue'
import App from './App.vue'
const app = createApp(App)
app.use(KontextAdsPlugin, {
publisherToken: '',
userId: 'user-1234',
conversationId: 'conv-5678',
character: {
id: 'character-1234',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
greeting: 'Hello, how can I help you today?',
},
onEvent: ({ name, code, payload }) => {
// process events
},
})
app.mount('#app')
```
If you'd rather scope a session to part of your component tree, use `` instead — it accepts the same options as the plugin.
```vue theme={null}
```
## 2. Feed messages to the SDK
The SDK preloads ads in response to new chat messages. Call `addMessage()` from the `useAds()` composable whenever a user or assistant message is created.
```vue theme={null}
```
## 3. Mount ``
An ad slot is a designated area in your UI where an ad can be rendered. In most cases, it appears below an assistant message.
Place `` wherever the ad should appear and pass the `messageId` of the assistant message it's associated with.
```vue theme={null}
```
## Observing events
Subscribe to ad lifecycle events via the plugin or `` `onEvent` prop. Every event has a stable `name` and a typed `payload`:
```ts theme={null}
app.use(KontextAdsPlugin, {
// ...
onEvent: (event) => {
switch (event.name) {
case 'ad.filled':
console.log('filled:', event.payload.id, 'revenue=', event.payload.revenue)
break
case 'ad.clicked':
console.log('clicked:', event.payload.url)
break
case 'ad.no-fill':
console.log('no fill, skipCode=', event.payload.skipCode)
break
}
},
})
```
Every placement-attributed event (`ad.filled`, `ad.viewed`, `ad.clicked`, `ad.render-*`, `video.*`, `reward.granted`) also carries a top-level `code` field naming the matched placement, so publishers with multiple `enabledPlacementCodes` can disambiguate. Session-wide events (`ad.no-fill`, `ad.error`) omit it.
For SDK-internal diagnostics during integration, also pass an `onDebugEvent` callback — it receives `(name, data?)` for every internal step.
# Guides
Source: https://docs.kontext.so/sdk/vue/guides
Patterns and recipes for common Vue SDK integration tasks.
## Handling no-fill
Subscribe to `ad.no-fill` to know when no ad was returned (geo-restriction, frequency cap, server-side skip). The `skipCode` payload tells you why.
```ts theme={null}
app.use(KontextAdsPlugin, {
// ...
onEvent: ({ name, payload }) => {
if (name === 'ad.no-fill') {
console.log('No ad available:', payload.skipCode)
}
},
})
```
Every preload produces exactly one of `ad.filled` / `ad.no-fill` / `ad.error` per response, so you can rely on hearing back on every user message.
## 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. 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. See [Pacing](/concepts/pacing) for the full pattern.
Use `{ trackOnly: true }` to send the preload for analytics without generating an ad:
```ts theme={null}
const { addMessage } = useAds()
const shouldShowAd = userMessageCount.value % 5 === 0
addMessage(
{ id: msg.id, role: 'user', content: msg.content, createdAt: new Date() },
{ trackOnly: !shouldShowAd }
)
```
When `trackOnly: true`, the preload still fires (server keeps full analytics) but no `ad.filled` event will arrive for that message and no `` will render content.
## Live-updating consent mid-session
The plugin (or ``) reactively passes new props into the underlying session. Bind `regulatory` to your CMP state and the next preload picks up the new value automatically:
```vue theme={null}
```
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, change the `conversationId` (and `character`) prop on ``. The provider tears down the previous session and spins up a new one automatically:
```vue theme={null}
```
The same applies to `publisherToken`, `userId`, `conversationId`, and `enabledPlacementCodes` — changing any of those rebuilds the session.
## Loading older messages (conversation restore)
When restoring a conversation from your backend, call `addMessage(...)` once per historical message in order. Preloads are debounced (\~10 ms), so rapid sequential calls coalesce into a single preload for the most recent user message — you won't fire one preload per restored message.
```ts theme={null}
const { addMessage } = useAds()
for (const m of loadedFromBackend) {
addMessage({
id: m.id,
role: m.role,
content: m.content,
createdAt: m.timestamp,
})
}
```
# Installation
Source: https://docs.kontext.so/sdk/vue/installation
Install @kontextso/sdk-vue.
You'll need a [publisher account](/publishers#getting-started-is-easy) to obtain your `publisherToken` and placement codes.
```bash theme={null}
npm install @kontextso/sdk-vue@4
```
That's it — there are no peer dependencies or platform-specific config to add. Head to [Showing your first ad](/sdk/vue/first-ad) to wire it up.