API Reference
The public surface of elo-ads-android. Anything not listed here is internal.
Singleton entry point. Configure once, load ads anywhere. All methods are @JvmStatic-friendly for Java callers.
object Elo {
// Configuration fun configure( context: Context, publisherId: String, adUnitId: String, shareGeoLocation: Boolean = true, geoLocationPrecision: Int? = 2, ) fun configure(context: Context, configuration: EloConfiguration)
// Loading suspend fun loadAd( messages: List<ChatMessage>, contextObjects: List<ContextObject> = emptyList(), maxHeight: Dp? = null, maxWidth: Dp? = null, ): AdResult
fun preloadAd( messages: List<ChatMessage>, contextObjects: List<ContextObject> = emptyList(), maxHeight: Dp? = null, maxWidth: Dp? = null, )
// Session control8 collapsed lines
fun enable() fun disable() val isDisabled: Boolean fun setShareGeoLocation(enabled: Boolean) fun setUserIdentifier(identifier: String?) fun setUserData(userData: EloUserData?) fun setUserIdentity(userIdentifier: String?, userData: EloUserData?) fun shutdown()
// Lifecycle observation fun setListener(listener: EloAdListener?)
// Your own in-app browser fun trackBrowserOpened(ad: EloAd) fun trackBrowserClosed(ad: EloAd)
// Debugging fun setDebugObserver( observer: ((EloDebugEvent) -> Unit)?, capturePayloads: Boolean = false, ) val sdkVersion: String}configure- The single entry point. Call once in
Application.onCreate()or your main Activity. The convenience overload covers Elo-only integrations (with the geo opt-out mirrored, since sharing is on by default); pass a fullEloConfigurationfor COPPA/TFUA, log level, orbaseUrl. loadAd- Requests an ad for the chat context. Returns a sealed
AdResult. Suspend function.maxHeightandmaxWidthdescribe the slot; the server picks the format that fits. See Ad Formats. preloadAd- Warms the in-memory cache for the given context. Fire-and-forget. The cache key is the messages, the context objects,
maxHeight,maxWidth, and the consent snapshot, so a preloaded ad is only served to a later load with the same messages and the same slot size. enable/disable- Pause and resume new ad requests. While disabled,
loadAdreturnsAdResult.NoFill(NoFillReason.SessionDisabled). setUserIdentifier- Sets a publisher-supplied user identifier (e.g. your account system’s user ID). When set, it is sent as the
visitor_idon ad requests and tracking pings in place of the SDK’s anonymous per-install ID, so delivery, frequency capping, and reporting follow the user across installs and devices. Persisted with the user data as one encrypted record, so it survives a relaunch and you set it once rather than on every launch — and it survives a sign-out, so passnullthere to restore the anonymous ID. Erased byshutdown(), not by a re-configure. Values are trimmed, a blank string clears, and identifiers over 256 characters are ignored with a warning. Also available on the iOS and web SDKs. setUserData- Shares first-party user data (age, gender, email, phone) on subsequent ad requests. Recorded for upcoming targeting features; no effect on ad selection today. Each call replaces the whole object;
nullclears it. Persisted with the identifier in the same encrypted record, so clear it on sign-out. Erased byshutdown(), not by a re-configure. SeeEloUserData. Also available on the iOS and web SDKs. setUserIdentity- Sets the identifier and the user data in one write. Prefer it to calling the two setters in sequence whenever both are changing (sign-in, sign-out, account switch): the two are persisted as one record, so two calls leave an intermediate state on disk in which one account’s contact details ride the other’s identifier. Each parameter behaves exactly as its single-field setter does, including the identifier’s trimming and length rules. Mirrors iOS
Elo.setUserIdentity. shutdown- Cancels requests, closes the HTTP client, releases resources.
setListener- Register an
EloAdListenerto receive lifecycle callbacks. trackBrowserOpened/trackBrowserClosed- Report your own in-app browser. Call
trackBrowserOpenedwhen your browser opens the ad’s click URL andtrackBrowserClosedwhen it closes; the SDK sendsbrowser_open, thenbrowser_closewith the time in between asdwell_ms. The time is measured on the device, counts time the app spent in the background or the device spent asleep, and is capped at 24 hours. A second open before the close, and a close without an open, send nothing, and so does a call beforeconfigure. Elo-served ads only. Analytics only: never billed. See Your own in-app browser. setDebugObserver- Reports each
EloDebugEventas it happens: load and preload outcomes, and tracking attempts and their outcomes. The SDK keeps no history. Passnullto clear the observer. For debug builds only. sdkVersion- Human-readable SDK version string to include in support tickets.
configure convenience parameters:
context- Android app context.
publisherId- Your publisher ID from Elo.
adUnitId- Your ad unit ID from Elo.
geoLocationPrecision- Decimal places used to round shared coordinates (~1.1 km at the default).
nullkeeps full precision.
loadAd parameters:
messages- Conversation context. Truncated to the most recent 30 messages before the request.
contextObjects- Optional screen / page metadata for richer targeting.
maxHeight- The height the slot can give the ad.
nullmeans unbounded, as in a scrolling chat feed. maxWidth- The width the slot can give the ad.
nullmeans unbounded.
The SDK sends maxHeight and maxWidth as the request’s dimensions, rounded down to whole dp. The server picks the format that fits the slot, the card or the strip, and the ad arrives with that format. EloAdView renders it. See Ad Formats.
EloConfiguration
Section titled “EloConfiguration”Full configuration object passed to Elo.configure(context, configuration).
data class EloConfiguration( val elo: EloNetworkConfiguration, val coppa: Boolean = false, val tfua: Boolean = false, val logLevel: LogLevel = LogLevel.Warn, val baseUrl: String? = null, val shareGeoLocation: Boolean = true, val geoLocationPrecision: Int? = 2,)
data class EloNetworkConfiguration( val publisherId: String, val adUnitId: String,)Key fields:
elo- Publisher / ad-unit IDs for the Elo network.
coppa/tfua- COPPA child-directed treatment and under-age tagging.
logLevelWarn(default),Info,Debug, orOff.baseUrl- API base-URL override. Leave
nullso the SDK uses its default endpoint. geoLocationPrecision- Decimal places used to round passive geo coordinates, clamped to 0–6. Default
2(~1.1 km) is privacy-conscious; setnullto keep full precision. Only applies whenshareGeoLocationis enabled.
EloUserData
Section titled “EloUserData”First-party user data passed to Elo.setUserData(...). Every field is optional - set only what you know.
data class EloUserData( val age: Int? = null, val gender: EloGender? = null, val email: String? = null, val phone: String? = null,)
enum class EloGender(val rawValue: String) { MALE("male"), FEMALE("female"), OTHER("other"),}Elo.setUserData(EloUserData(age = 34, gender = EloGender.FEMALE, email = account.email))Elo.setUserData(null) // sign-out; or Elo.setUserIdentity(null, null) to clear bothage- Age in years. Child-directed traffic is gated by
coppa/tfua, not by this value. genderMALE,FEMALE, orOTHER.rawValueis the wire form - stable across platforms, don’t localize it.email- Email address, as you hold it. Case and surrounding whitespace don’t matter - the ad server normalizes both before hashing.
phone- Phone number with its country code (E.164, e.g.
"+15551234567"). The server strips formatting punctuation; a number without a country code is ambiguous and is discarded rather than guessed at.
Rules worth knowing:
- Pass contact details as they are. They travel over HTTPS and are SHA-256 hashed at the ad server before anything is stored, so Elo never persists a plain email address or phone number. Hashing them yourself produces a digest the server can’t reproduce.
- The SDK does not validate what you set. Values are forwarded as supplied; the server normalizes each field and drops what it can’t use, so there is one set of rules rather than two that can disagree.
- Each call replaces the whole object.
nullclears it, as does an object with no field set. - Persisted on the device, encrypted at rest. The data is stored with the
setUserIdentifiervalue as one record, sealed with AES-GCM under a non-exportable Android Keystore key and kept as ciphertext in the SDK’s ownSharedPreferences, then read back at launch — so you set it once rather than on every launch. If the Keystore is unavailable the SDK keeps it in memory for that process; nothing is ever written in plaintext.shutdown()erases the stored record; a re-configure does not. - It survives a sign-out, so clearing it is an obligation. Without
Elo.setUserData(null)the previous account’s details keep riding this device’s requests. Set this and the identifier from one auth-state hook: that call site covers sign-in, sign-out, and an account switch, and it is what keeps the stored copy from going stale when the user changes their email or phone. When both are changing, write them withElo.setUserIdentity(userIdentifier, userData)rather than the two setters in sequence, which briefly stores one account’s details under the other’s identifier. - The data never joins tracking pings, and the ad server discards it entirely on requests flagged COPPA or TFUA, and on any request where GDPR applies.
- Transmitting an email address and phone number is a disclosure obligation - see Privacy & Consent.
AdResult
Section titled “AdResult”Sealed class returned by loadAd(). Use exhaustive when to handle every variant.
sealed class AdResult { data class Loaded( val ad: EloAd, val eCpm: Double, ) : AdResult() data class NoFill(val reason: NoFillReason) : AdResult() data class Error(val error: EloError) : AdResult()}Loaded- Elo filled the request.
eCpmis the price the Elo server quoted for the ad (USD-equivalent CPM, always>= 0). Use it to compare against another network. Cache-served preloads return the price of the original fill. NoFill- No ad was served. The reason describes why - see
NoFillReason. Not an error. Treat it likeErrorwhen falling back to another network. Error- Something went wrong.
EloErroris a sealedRuntimeExceptioncarrying a numericerrorCode, message, and optional cause.
NoFillReason
Section titled “NoFillReason”sealed class NoFillReason { data object NoBids : NoFillReason() data object Timeout : NoFillReason() data object SessionDisabled : NoFillReason() data object NotConfigured : NoFillReason() data object NetworkError : NoFillReason() data class Other(val message: String) : NoFillReason()}NoBids means the Elo server answered with no ad, Timeout that the request did not complete within the SDK’s deadline (3 seconds), and NetworkError that it failed with a transport or server error.
The ad returned in AdResult.Loaded. Render it with EloAdView, which sends its tracking.
data class EloAd( val id: String, val creativeId: String, val title: String, val description: String?, val imageUrl: String?, val ctaText: String?,) { // Display-only ad for previews and tests. It sends no events. constructor( id: String, creativeId: String, title: String, description: String?, imageUrl: String?, ctaText: String? = null, clickUrl: String, renderer: AdRenderer? = null, )
fun releaseResources()}An EloAd is one showing, not one creative: two ads with the same creativeId and different ids are two opportunities, each with its own tracking.
Only the SDK’s loader builds an ad that sends tracking. The public constructor builds a display-only ad for previews and tests: EloAdView renders it but sends no render, impression, click, or tap events. The click URL is not a public property.
id- The ad opportunity: one per showing. The ad server keys render, impression, and click under it, so it is the value that correlates a confirmed impression to its funnel row, and what the SDK’s render/impression dedup keys on.
creativeId- The creative that filled the opportunity. Stable across showings, since the same creative is served to the same user many times. Use it to recognize a creative for frequency or creative-level reporting; never to identify a showing.
ctaText- The creative’s own call-to-action label (
Install,Learn more), when the demand source supplies one.EloAdViewdraws it as a pill in the trailing slot in place of the disclosure chevron, on both the card and the strip.nullon creatives that send no label. CTA copy is backend-managed: there is no publisher-supplied override. releaseResources()- Call only when you discard the ad without rendering it (e.g. you compared prices and picked another network). Don’t call on ads you pass to
EloAdView. Safe to call more than once. Named for parity with iOS.
EloAdView
Section titled “EloAdView”The single Compose ad surface. Handles render, impression, and click tracking automatically.
@Composablefun EloAdView( messages: List<ChatMessage>, modifier: Modifier = Modifier, contextObjects: List<ContextObject> = emptyList(), maxHeight: Dp? = null, maxWidth: Dp? = null, style: EloAdStyle = EloAdStyle.Default, sponsoredLabel: EloAdDisclosure = EloAdDisclosure.Default, showLoadingPlaceholder: Boolean = true, loadingAccessibilityLabel: String = "Loading sponsored content", onResult: (AdResult) -> Unit = {},)
@Composablefun EloAdView( result: AdResult, modifier: Modifier = Modifier, style: EloAdStyle = EloAdStyle.Default, sponsoredLabel: EloAdDisclosure = EloAdDisclosure.Default,)
@Composablefun EloAdView( ad: EloAd, modifier: Modifier = Modifier, style: EloAdStyle = EloAdStyle.Default, sponsoredLabel: EloAdDisclosure = EloAdDisclosure.Default,)EloAdView is the only public loaded-ad surface. It renders each ad in the format the server chose for it, and respects EloAdStyle. You do not pick the format; see Ad Formats.
- Card: the chat-row card, set like a feed ad: a header row with a 36dp brand mark beside the brand name and the disclosure under it, the description as body copy at the card’s full width (up to three lines), and a CTA pill on a row of its own at the end edge, or a chevron closing the header row without one. Its height follows the creative, from 92dp to 190dp, so the server returns it for an unbounded slot or one at least 192dp tall. The whole card is one click target, with or without a CTA pill.
- Strip: the two-line strip (thumbnail, attribution, one-line description, and a CTA pill or chevron) for slots shorter than 192dp, such as the composer or keyboard.
EloKeyboardBannerAdrenders it. When the creative carries anEloAd.ctaText, only the CTA pill clicks; without a label the whole row stays clickable behind the chevron.
The messages overload calls Elo.loadAd(...) internally with its maxHeight and maxWidth. It shows a loading skeleton while loading, renders the ad on fill, calls onResult with the final AdResult, and collapses on NoFill or Error. The skeleton follows maxHeight: the strip’s skeleton below 192dp, the card’s otherwise. Pass showLoadingPlaceholder = false when the host layout should reserve no ad space during loading. It starts a new request when messages, contextObjects, maxHeight, or maxWidth changes.
The loaded-ad overloads send render, impression, and click tracking themselves: render on first composition, the impression once the ad is ≥50% visible for ≥1 continuous second (MRC standard, hardcoded), and the click when tapped, before the click URL opens. Render and impression are recorded at most once per ad opportunity, for the life of the process, so recomposition or a reconfigure cannot report an opportunity twice. Pass an AdResult to the result overload to have the view collapse on NoFill or Error.
The dwell also requires a resumed app, a focused host window, and an attached, visible, non-transparent native host hierarchy. Backgrounding, opening another window, hiding or detaching the host, or making its native hierarchy effectively transparent cancels an armed dwell, and returning to a visible state starts a fresh one; the SDK re-checks all of it when the dwell finishes rather than trusting state measured a second earlier. A scaled or otherwise transformed Compose host is measured entirely in window space.
messages- Conversation context for the internally-loading overload.
contextObjects- Optional screen / page metadata for the internally-loading overload.
maxHeight- The height the slot can give the ad, sent with the request.
nullmeans unbounded. Also picks the loading skeleton. maxWidth- The width the slot can give the ad, sent with the request.
nullmeans unbounded. sponsoredLabel- The ad disclosure: a line under the brand name on the card, a badge on the creative mark’s top-end corner on the strip — see EloAdDisclosure.
showLoadingPlaceholder- Whether the internally-loading overload shows a skeleton while the ad request is in flight.
loadingAccessibilityLabel- Accessibility label for the built-in loading placeholder.
onResult- Callback invoked when the internally-loading overload receives the final
AdResult.
EloAdView and EloKeyboardBannerAd also report taps inside an Elo-served ad to Elo analytics as an ad_tap event: the region under the finger (thumbnail, headline, description, CTA, chevron, or chrome), the tap position as a fraction of the view’s own size, the view’s size in dp, whether the tap opened the destination, the time since the impression, and a per-showing tap index. Coordinates are relative to the ad view and never to the screen, and at most fifty taps are reported per showing. It is analytics only: never billed, and separate from click tracking. Declare it as App interactions on your Data safety form; see Privacy & Consent.
The clickable surfaces pass no onClickLabel: TalkBack announces the content description and the button role, and the “Ad” disclosure carries that it is an ad. Call-to-action copy is backend-managed and arrives on the creative as EloAd.ctaText, so there is nothing publisher-supplied to pass here.
EloKeyboardBannerAd
Section titled “EloKeyboardBannerAd”Pins the strip format above the software keyboard. Owns its own request lifecycle: sends the strip’s 76dp height as the request’s maxHeight, renders on fill, collapses to zero height on no-fill or error, and reloads when messages changes.
@Composablefun EloKeyboardBannerAd( messages: List<ChatMessage>, modifier: Modifier = Modifier, contextObjects: List<ContextObject> = emptyList(), style: EloAdStyle = EloAdStyle.Default, sponsoredLabel: EloAdDisclosure = EloAdDisclosure.Default, showLoadingPlaceholder: Boolean = true, loadingAccessibilityLabel: String = "Loading sponsored content", onResult: (AdResult) -> Unit = {},)The strip tracks the IME via WindowInsets.ime and rests above the navigation bar when the keyboard is hidden. Pinning requires an edge-to-edge window with adjustResize; it degrades to the window bottom otherwise. showLoadingPlaceholder defaults to true, matching EloAdView: the skeleton stands at the strip’s own size, so the composer above keeps its place when an ad arrives. Pass false to keep the keyboard edge clear until an ad fills. See Keyboard banner for placement guidance and host-side caveats.
When the creative carries an EloAd.ctaText, only the CTA button clicks: the strip sits under the reader’s thumb beside the composer, where an edge-to-edge target invites accidental clicks. Without a label the whole row stays clickable behind the chevron.
EloAdLoadingView
Section titled “EloAdLoadingView”Skeleton placeholder for callers that own loading state outside the internally-loading EloAdView(messages = ...) overload.
@Composablefun EloAdLoadingView( modifier: Modifier = Modifier, style: EloAdStyle = EloAdStyle.Default, showCtaPlaceholder: Boolean = false, loadingAccessibilityLabel: String = "Loading sponsored content",)EloAdLoadingView draws the card’s skeleton. It is not an ad impression and does not fire render, impression, or click tracking. Pass the same EloAdStyle you use for the loaded ad to keep the reserved slot visually aligned. showCtaPlaceholder is ignored: whether a card draws a CTA button follows the creative and isn’t known while the request is in flight, so the skeleton reserves no CTA slot. The parameter is kept for source compatibility.
EloAdDisclosure
Section titled “EloAdDisclosure”The ad disclosure. On the card it is a line of text under the brand name in the header row. On the strip it is a small badge hung on the creative mark’s top-end corner, or on the strip’s own top-end corner when a creative’s image fails to load. Passed as sponsoredLabel to every EloAdView overload and to EloKeyboardBannerAd.
@Stabledata class EloAdDisclosure( val text: String = "Ad", val fontSize: TextUnit? = null, val fontWeight: FontWeight? = null, val color: Color? = null, val testTag: String? = null,) { companion object { val Default: EloAdDisclosure }}text- Disclosure copy, localized by you. Trimmed before rendering; blank drops the disclosure rather than drawing an empty line or capsule.
fontSizenullkeeps the SDK’s type: 12sp under the brand name on the card, 8sp on the strip’s badge. Setting a size pins one across both.fontWeightnullkeeps the SDK’s weight: regular on the card, medium on the strip’s badge.color- The text color on the card. On the strip it fills the badge, and the copy is drawn over it in the strip color.
nullfalls back toEloAdStyle.badgeColor, which restyles every surface at once, and then to the SDK’s secondary text color on the card and its own near-white disc with a hairline black ring and black copy on the strip. Those are the only fixed colors in the SDK, because the badge sits on creative artwork rather than on a surface the theme controls. Supplying a color takes the ring off and inverts the copy, so pick one with contrast of its own. testTag- Names the disclosure node for Compose UI tests. It is its own node, separate from the headline’s text.
The SDK draws the disclosure itself, where nothing can truncate or displace it, which is why this is a value and not a composable slot. On the strip the badge is overlaid on the creative rather than laid out beside the headline, so it costs the headline no width and adds no height to the row. It hangs off the mark’s corner rather than sitting flush inside it, and it is squared up from a capsule for short copy (the default is two characters), relaxing back into a capsule as the copy gets longer. Its line box is trimmed to the glyphs, so it does not inherit the ambient body line height.
EloAdView( messages = messages, sponsoredLabel = EloAdDisclosure(stringResource(R.string.elo_sponsored)),)EloAdStyle
Section titled “EloAdStyle”Optional styling overrides passed to any EloAdView. Only non-null fields are applied - everything else uses your Material 3 theme.
@Stabledata class EloAdStyle( val cardBackground: Color? = null, val titleColor: Color? = null, val descriptionColor: Color? = null, val badgeColor: Color? = null, val callToActionBackground: Color? = null, val callToActionForeground: Color? = null, val borderColor: Color? = null, val borderWidth: Dp? = null, val cornerRadius: Dp? = null, val descriptionOverflow: EloAdDescriptionOverflow? = null, val fontFamily: FontFamily? = null, val chrome: EloAdChrome? = null,) { companion object { val Default: EloAdStyle }}
enum class EloAdChrome { Standard, Bare }cardBackground- Card background color.
titleColor- Ad title text color.
descriptionColor- Ad description text color.
badgeColor- The disclosure text color on the card, the fill of the disclosure badge on the strip, and the color of the trailing chevron. On the strip the disclosure copy is drawn over the badge in the strip color, and setting this takes the badge’s default ring off. An
EloAdDisclosure.coloroverrides it for that one surface. callToActionBackground- Fill of the CTA pill drawn for creatives that carry an
EloAd.ctaText. Defaults to the theme’s primary. Mirrors iOSEloAdStyle.callToActionBackground. callToActionForeground- CTA pill text color.
nulluses the theme’s onPrimary on the default fill, and black or white, whichever contrasts more, on acallToActionBackgroundyou set. borderColor- Card border color (requires
borderWidthto render). borderWidth- Card border width.
cornerRadius- Corner radius of the surface. Defaults to 14.dp on the card and 16.dp on the strip.
descriptionOverflow- How an over-long description behaves on the strip:
Marquee(default) scrolls it on one line;Truncatetail-truncates.nullusesMarquee. The card wraps its description to up to three lines and ends longer copy with an ellipsis, whatever this is. fontFamily- Typeface for the headline, description, CTA label, and disclosure badge. The SDK keeps its own sizes and weights.
nullkeeps the theme’s family. chromeStandard(default) draws the background, border, and rounded corners.Baredraws none of them, so the ad sits inside your own row; the card also drops all of its inner padding, and the strip keeps its padding for the tap inset.EloAdLoadingViewfollows it.
The SDK has no light or dark switch of its own. To force a color scheme on the ad alone, wrap it in your own MaterialTheme.
EloAdDescriptionOverflow is an enum with values Marquee and Truncate; marquee makes a few passes, then settles into an ellipsized line, and falls back to a static ellipsis when the system has animations disabled or reduced-motion is on.
For the disclosure copy, which varies per call site, pass an EloAdDisclosure as sponsoredLabel instead.
EloAdListener
Section titled “EloAdListener”Lifecycle callbacks for an ad. Register with Elo.setListener(...). All methods have default implementations that do nothing (onAdOpenClickUrl returns false), so you only override what you care about.
interface EloAdListener { fun onAdDidLoad(ad: EloAd) {} fun onAdDidFail(error: EloError) {} fun onAdDidTrackImpression(ad: EloAd) {} fun onAdDidReceiveClick(ad: EloAd) {} fun onAdOpenClickUrl(ad: EloAd, url: String): Boolean = false}onAdDidTrackImpression fires once per ad opportunity, and only after the ad server confirms the impression ping: a ping that fails or is rejected fires nothing, so counting impressions from this callback matches what Elo reports.
When the user taps an ad, onAdOpenClickUrl is called on the main thread first. Open url, the ad’s click URL, in your own browser, such as a Custom Tab, and return true; return false and the SDK opens it in the system browser, as it does when the callback throws. onAdDidReceiveClick follows, right after the SDK tries to open the URL. It is also called when no browser could open the URL, so it does not prove a page opened. A repeat tap within a second of an open opens nothing and calls neither method. See Your own in-app browser.
EloError is a sealed RuntimeException carrying a numeric errorCode, message, and optional cause, with subtypes for NotConfigured, NetworkError, ServerError, DecodingError, Timeout, and Cancelled. Surfaced via EloAdListener.onAdDidFail and AdResult.Error — match on the subtype to drive retry, fallback, or diagnostic UI.
ChatMessage & MessageRole
Section titled “ChatMessage & MessageRole”Input types for Elo.loadAd().
enum class MessageRole { USER, ASSISTANT, SUMMARY,}
data class ChatMessage @JvmOverloads constructor( val role: MessageRole, val content: String, val id: String? = null, val createdAt: Instant? = null,)Pass your recent conversation as a List<ChatMessage> to get a contextually relevant ad. Use MessageRole.SUMMARY to send a condensed description of the conversation in place of the full history - a typical pattern is one SUMMARY message followed by the most recent USER / ASSISTANT exchange. See Load an ad for examples.
The roles are named for AI-chat hosts, but human-to-human messengers use the same two values: map the device owner to USER and the other participant to ASSISTANT. Only the split between “this user” and “the other side” matters for contextual targeting.
id and createdAt (a java.time.Instant) are optional. Give each message the id and time your app already keeps for the turn: the SDK sends them with the message, so Elo knows when each turn happened and can connect a turn to a Search API call that sends the same id as X-Elo-Message-Id. Set them once, when the turn is created, and reuse them on every request. Without createdAt, Elo records the time it received the message, which is the time of the ad request that carried it. A MessageRole.SUMMARY message should carry the id and createdAt of the turn it summarizes. An id longer than 256 Unicode code points or containing non-printable characters, and a createdAt before 2020 or more than a day ahead, are left out of the request with a warning. Neither affects the preload cache.
ChatMessage( role = if (message.isFromLocalUser) MessageRole.USER else MessageRole.ASSISTANT, content = message.text, id = message.id, createdAt = message.sentAt,)Targeting types
Section titled “Targeting types”data class ContextObject( val type: String, val description: String,)ContextObject- Screen / page metadata attached to a
loadAdrequest for richer targeting.
Debug observer
Section titled “Debug observer”Elo.setDebugObserver(observer, capturePayloads) reports the SDK’s internal events as they happen: each load and preload outcome, and every tracking attempt and its outcome. The types live in ad.elo.androidsdk.debug.
if (BuildConfig.DEBUG) { Elo.setDebugObserver { event -> Log.d("elo", event.toString()) }}The SDK stores none of it. It reports each EloDebugEvent and keeps no history, so an observer that wants a recent-loads list or tracking totals records the events itself. With no observer attached the SDK does no debug work and stores no load history, tracking history, or request payloads. The observer may be called from any thread and is never called while the SDK holds an internal lock, so it can call back into Elo. Unlike setListener, the observer is held strongly; pass null to clear it. It is a process-level setting, independent of configure and shutdown, so set it once, for example at app startup.
This is a debugging aid for sample apps and in-app diagnostics, not a stable analytics pipeline. Use EloAdListener for ad lifecycle callbacks in production. Events carry request payloads, ad-opportunity ids, and timestamps: keep them on the device.
sealed interface EloDebugEvent { data class Configured( val publisherId: String, val adUnitId: String, val requestTimeoutMs: Long, val validationIssueCount: Int, val timestampEpochMs: Long, ) : EloDebugEvent data object Shutdown : EloDebugEvent data class RequestPayloadCaptured( val operationId: String, val json: String, ) : EloDebugEvent data class ResponsePayloadCaptured( val operationId: String, val json: String, ) : EloDebugEvent data class ServerRequestIdAssigned( val operationId: String, val serverRequestId: String, ) : EloDebugEvent data class AdOperation( val operationId: String, val timestampEpochMs: Long, val messageCount: Int, val contextCount: Int, val latencyMs: Long, val outcome: EloAdLoadOutcome, val format: String? = null, val maxHeight: Int? = null, val maxWidth: Int? = null, val ctaEnabled: Boolean? = null, ) : EloDebugEvent data class TrackingAttempted( val operationId: Long, val adId: String, val event: EloTrackingEvent, val serverRequestId: String?, val timestampEpochMs: Long, ) : EloDebugEvent data class TrackingCompleted( val operationId: Long, val event: EloTrackingEvent, val outcome: EloTrackingOutcome, val timestampEpochMs: Long, ) : EloDebugEvent}Configuredconfigurecompleted.requestTimeoutMsis the SDK’s deadline for one ad request, andvalidationIssueCountis the number of problems the configuration validator found (0means a clean integration). Every earlier event belongs to a previous configuration, so discard your history here.Shutdownshutdown()ran. All SDK state is gone.RequestPayloadCaptured- The redacted JSON body of an outgoing ad request. Sent only when
capturePayloadsistrue. Correlate withAdOperation.operationId. ResponsePayloadCaptured- The decoded ad response, encoded again as JSON for inspection. It is not the literal server bytes. Same opt-in and correlation as
RequestPayloadCaptured. ServerRequestIdAssigned- The server’s ad-opportunity id for a request. The ad server keys that request’s impression and click events under the same id.
AdOperation- A
loadAdorpreloadAdfinished. It carries message and context counts, never their content;contextCountis capped at 999.formatis the format the ad rendered as (striporcard) andnullwithout a fill.maxHeightandmaxWidthare the requested slot in whole dp,nullwhen unbounded.ctaEnabledis the ad’scta_enabledrender setting,nullwhen not sent or without a fill. TrackingAttempted- A tracking operation started.
serverRequestIdis the ad opportunity,EloAd.id. Its outcome arrives later as aTrackingCompletedwith the sameoperationId. TrackingCompleted- A tracking operation resolved with an
EloTrackingOutcome.
sealed interface EloAdLoadOutcome { data class Loaded(val eCpm: Double) : EloAdLoadOutcome data class NoFill(val reason: NoFillReason) : EloAdLoadOutcome data class Error(val category: EloErrorCategory) : EloAdLoadOutcome}
sealed interface EloErrorCategory { data object Network : EloErrorCategory data object Decoding : EloErrorCategory data object InternalState : EloErrorCategory}
enum class EloTrackingEvent { RENDER, IMPRESSION, CLICK, TAP, BROWSER_OPEN, BROWSER_CLOSE, VIEWABLE,}
sealed interface EloTrackingOutcome { data object Delivered : EloTrackingOutcome data object Unobservable : EloTrackingOutcome data class Failed(val category: EloTrackingFailureCategory) : EloTrackingOutcome}
sealed interface EloTrackingFailureCategory { data object Network : EloTrackingFailureCategory data class HttpStatus(val code: Int) : EloTrackingFailureCategory data object Cancelled : EloTrackingFailureCategory}EloAdLoadOutcome.Loaded carries the price the server quoted for the ad. EloErrorCategory is deliberately coarse: Network is a transport failure, Decoding a response the SDK could not parse, and InternalState a loadAd called outside a valid SDK state.
RENDER, IMPRESSION, and CLICK are the events EloAdView sends. TAP covers every tap inside an Elo-served ad view, whether or not it opened the destination; see EloAdView. BROWSER_OPEN and BROWSER_CLOSE are the calls to Elo.trackBrowserOpened and Elo.trackBrowserClosed. VIEWABLE is sent once when an Elo-served ad first meets the viewability bar, with the ad view’s size and position in the window, the window size, the visible fraction, and the time since render; the impression is recorded separately. TAP, BROWSER_OPEN, BROWSER_CLOSE, and VIEWABLE are analytics only and never billed. An exhaustive when over EloTrackingEvent needs a branch for each value.
Delivered means every required Elo tracking endpoint returned 2xx, and Failed means delivery failed, with a Network, HttpStatus, or Cancelled category. Unobservable means the attempt completed on the device but the server’s receipt cannot be observed: Elo-direct click delivery is currently disabled, so every Elo-network click reports this outcome by design. Tracking events never include tracking URLs, request headers, or chat content.
Payload capture
Section titled “Payload capture”Pass capturePayloads = true to also receive each operation’s redacted request payload and decoded ad response as RequestPayloadCaptured and ResponsePayloadCaptured:
Elo.setDebugObserver({ event -> Log.d("elo", event.toString()) }, capturePayloads = true)