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.

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.
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:
DisposableEffect(session) {
    onDispose { session.close() }
}
In a ViewModel:
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.
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.
When the user updates their consent in your CMP, call:
import so.kontext.ads.model.Regulatory

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(
    context = applicationContext,
    options = 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, 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 (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 for the full pattern.
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.
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):
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.