API Reference
This page covers the public surface of the EloAds module. For running Elo alongside another ad network, see Other Ad Networks.
Caseless enum entry point. Configure once, then load ads anywhere.
public enum Elo { public static func configure( publisherId: String, adUnitId: String, shareGeoLocation: Bool = true, geoLocationPrecision: Int? = 2 )
public static func configure(with configuration: EloConfiguration) public static var isConfigured: Bool { get }
public static func loadAd( messages: [ChatMessage], contextObjects: [ContextObject] = [], maxHeight: CGFloat? = nil, maxWidth: CGFloat? = nil ) async -> AdResult
public static func loadAd<M: MessageRepresentable>( messages: [M], contextObjects: [ContextObject] = [], maxHeight: CGFloat? = nil, maxWidth: CGFloat? = nil ) async -> AdResult
public static func preloadAd( messages: [ChatMessage], contextObjects: [ContextObject] = [], maxHeight: CGFloat? = nil, maxWidth: CGFloat? = nil )
11 collapsed lines
public static func preloadAd<M: MessageRepresentable>( messages: [M], contextObjects: [ContextObject] = [], maxHeight: CGFloat? = nil, maxWidth: CGFloat? = nil )
public static func enable() public static func disable() public static var isDisabled: Bool { get } public static func setShareGeoLocation(_ enabled: Bool) public static func setUserIdentifier(_ identifier: String?) public static func setUserData(_ data: EloUserData?) public static func setUserIdentity(userIdentifier: String?, userData: EloUserData?) public static func shutdown()
public static func setDelegate(_ delegate: EloAdDelegate?)
public static func trackBrowserOpened(_ ad: EloAd) public static func trackBrowserClosed(_ ad: EloAd)
public static func setDebugObserver( _ observer: (@Sendable (EloDebugEvent) -> Void)?, capturePayloads: Bool = false )
public static var sdkVersion: String { get }}configure(publisherId:adUnitId:...)- Sets up the SDK for Elo-only integrations. Call once early in your app lifecycle, typically
App.init. Only the geo-sharing controls are mirrored here, so the on-by-default opt-out stays visible at the simplest entry point. configure(with:)- Full form when you need COPPA / TFUA flags, log-level control, or a
baseUrloverride. Subsequent calls re-configure the SDK and reset the session ID. loadAd- Requests an ad for the chat context and the slot size. Returns an
AdResult. 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 served only to a later load with the same inputs. Pass the samemaxHeightandmaxWidthtopreloadAdand to the load that should use the ad. enable/disable- Pause and resume new ad requests. While disabled,
loadAdreturns.noFill(.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 Keychain record, so it survives a relaunch and you set it once rather than on every launch — and it survives a sign-out, so passnilthere 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 Android 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;
nilclears 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 Android 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.
shutdown- Cancels requests, invalidates the URLSession, and releases resources.
setDelegate- Register an
EloAdDelegateto receive lifecycle callbacks. Held weakly, callbacks on@MainActor. 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, and is capped at 24 hours. A second open before the close, and a close without an open, send nothing. Elo-served ads only. Analytics only: never billed. See Your own in-app browser. setDebugObserver- Registers a closure that receives each
EloDebugEventas it happens: load and preload outcomes, and tracking attempts with their outcomes. The SDK keeps no history of its own. Passnilto stop observing. For debug builds and on-device inspection only. sdkVersion- Human-readable SDK version string to include in support tickets.
configure parameters:
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).
nilkeeps full precision.
loadAd parameters:
messages- Conversation context. The SDK forwards the latest 30 messages.
contextObjects- Optional screen or page metadata for richer targeting.
maxHeight- The most height, in points, the slot can give the ad.
nilmeans unbounded. Rounded down to whole points. maxWidth- The most width, in points, the slot can give the ad.
nilmeans unbounded. Rounded down to whole points.
You describe the slot and the server chooses the format. The server returns the format that fits maxHeight and maxWidth on the ad, and EloAdView renders that format: the card needs at least 192 points of height, and the strip fits a slot from 72 to 191 points. When no format fits, the result is .noFill. Leaving maxHeight nil returns the card. Elo.preloadAd and EloChatSession.loadAd take the same two parameters. See Ad Formats.
EloConfiguration
Section titled “EloConfiguration”Full configuration object passed to Elo.configure(with:). Use this when the convenience configure doesn’t expose what you need — COPPA / TFUA, log level, or baseUrl.
public struct EloConfiguration: Sendable { public var elo: EloNetworkConfiguration public var coppa: Bool public var tfua: Bool public var logLevel: EloLogLevel public var baseUrl: URL public var shareGeoLocation: Bool public var geoLocationPrecision: Int?
public init( elo: EloNetworkConfiguration, coppa: Bool = false, tfua: Bool = false, logLevel: EloLogLevel = .warn, baseUrl: URL = EloConfiguration.defaultBaseUrl, shareGeoLocation: Bool = true, geoLocationPrecision: Int? = 2 )}
public struct EloNetworkConfiguration: Sendable { public let publisherId: String public let adUnitId: String
public init(publisherId: String, adUnitId: String)}Elo.configure( with: EloConfiguration( elo: EloNetworkConfiguration( publisherId: "YOUR_PUBLISHER_ID", adUnitId: "YOUR_AD_UNIT_ID" ), logLevel: .warn ))Key fields:
elo- First-party Elo demand configuration.
coppa- COPPA child-directed treatment flag. Defaults to
false. tfua- Under-age treatment flag. Defaults to
false. logLevel- SDK logging threshold. Defaults to
.warn. baseUrl- API endpoint for Elo ad requests. Defaults to production.
geoLocationPrecision- Decimal places used to round passive geo coordinates, clamped to 0–6. Default
2(~1.1 km) is privacy-conscious; setnilto 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.
public struct EloUserData: Sendable, Equatable { public var age: Int? public var gender: EloGender? public var email: String? public var phone: String?
public init( age: Int? = nil, gender: EloGender? = nil, email: String? = nil, phone: String? = nil )}
public enum EloGender: String, Sendable, Equatable { case male case female case other}Elo.setUserData(EloUserData(age: 34, gender: .female, email: account.email))Elo.setUserData(nil) // sign-out; or Elo.setUserIdentity(userIdentifier: nil, userData: nil) to clear bothage- Age in years. Child-directed traffic is gated by
coppa/tfua, not by this value. gender.male,.female, or.other.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.
nilclears 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 Keychain record, aThisDeviceOnlyitem so it never reaches iCloud Keychain or an encrypted device backup, and read back at launch — so you set it once rather than on every launch. If the Keychain 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(nil)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.
EloLogLevel
Section titled “EloLogLevel”public enum EloLogLevel: Int, Comparable, Sendable { case verbose case debug case info case warn case error case none}AdResult
Section titled “AdResult”Enum returned by loadAd(messages:). switch exhaustively to handle every case.
public enum AdResult: Sendable { case loaded(EloAd, eCpm: Double) case noFill(NoFillReason) case error(message: String)}.loaded(let ad, let eCpm)- 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(let reason)- No ad was served. The reason describes why. Not an error.
.error(let message)- Something went wrong, such as a transport or decoding failure.
NoFillReason
Section titled “NoFillReason”public enum NoFillReason: Sendable, Equatable { case noBids case timeout case notConfigured case networkError case sessionDisabled case other(String)}.noBids- The Elo server answered with no ad for this request.
.timeout- The request did not complete within the SDK’s deadline (3 seconds).
.notConfiguredloadAdwas called beforeconfigure, or aftershutdown()..networkError- The ad-server request failed with a transport or server error.
.sessionDisabledElo.disable()was called and the SDK is currently paused..other(let message)- A reason that doesn’t fit the categories above.
The ad data. Pass to EloAdView to display.
public struct EloAd: Sendable, Hashable, Identifiable { public let id: String public let creativeId: String public let title: String public let description: String? public let imageUrl: String? public let ctaText: String? public let clickUrl: String?
public init( id: String, creativeId: String, title: String, description: String?, imageUrl: String?, ctaText: String? = nil, clickUrl: String? = nil, renderer: AdRenderer? = nil )
public func releaseResources()}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 place of the disclosure chevron: in the strip’s trailing slot, or on the card’s own row under the copy.nilon creatives that send no label. CTA copy is backend-managed: there is no publisher-supplied override. init(...)- Builds a display-only ad for SwiftUI previews and tests. It sends no render, impression, click, or tap events.
idmust be unique per fill, not per creative. releaseResources()- Call only when you discard the ad without rendering it (for example, you compared prices and picked another network). Don’t call it on ads you pass to
EloAdView. Safe to call more than once.
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. Equality and hashing follow id.
The ad also carries the format the server chose for it, which EloAdView reads. The format and the tracking handles are package-internal. There is no public API to send render, impression, or click tracking yourself: render the ad with EloAdView or .eloKeyboardBannerAd.
EloAdView
Section titled “EloAdView”EloAdView renders an ad and sends render, impression, click, and tap tracking itself. It is the only way to send that tracking.
public struct EloAdView: View { public init( ad: EloAd, sponsoredLabel: EloAdDisclosure = .default )
public init( result: AdResult, sponsoredLabel: EloAdDisclosure = .default )
public init( result: AdResult?, sponsoredLabel: EloAdDisclosure = .default )
public init<M: MessageRepresentable>( messages: [M], contextObjects: [ContextObject] = [], maxHeight: CGFloat? = nil, maxWidth: CGFloat? = nil, sponsoredLabel: EloAdDisclosure = .default, showLoadingPlaceholder: Bool = true, loadingAccessibilityLabel: String = "Loading sponsored content", releaseLoadedAdOnDisappear: Bool = true, onResult: @escaping (AdResult) -> Void = { _ in } )}Use init(result:) to pass an AdResult directly. The view collapses to nothing on .noFill(_), .error(_), or nil. init(ad:) and init(result:) render the format the ad carries.
init(messages:) owns the request: it calls Elo.loadAd with the messages, context objects, maxHeight, and maxWidth, shows EloAdLoadingView while the request is in flight, renders the ad on fill, and collapses on no-fill or error. It does not send EloChatSession.events.
sponsoredLabel- The ad disclosure: a line under the brand name on the card, a badge on the creative mark’s top-trailing corner on the strip — see EloAdDisclosure. A string literal converts implicitly; a
Stringvariable needs the explicit wrap. maxHeight/maxWidth- The slot size, in points, sent with the request. The server chooses the format that fits; see
loadAdparameters. The loading placeholder draws the strip below 192 points and the card otherwise. showLoadingPlaceholder- Show the skeleton while the request is in flight.
loadingAccessibilityLabel- VoiceOver label for the skeleton.
releaseLoadedAdOnDisappear- Release the loaded ad when the view disappears. Set it to
falsewhenonResultmoves the result into longer-lived state that renders it withinit(result:). onResult- Reports each load outcome.
Formats
Section titled “Formats”The server chooses the format and EloAdView renders it. Two formats exist today; see Ad Formats.
| Format | Description |
|---|---|
| Card | A chat-row card set like a feed ad: a header row with the 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 trailing edge, or a chevron closing the header row without one. Its height follows the creative, from 87 to 175 points, so it needs a slot of at least 192 points; an unbounded slot gets it. |
| Strip | A two-line strip (thumbnail, attribution, one-line description, and a CTA pill or chevron) for persistent slots anchored to the composer or keyboard. Fits a slot from 72 to 191 points tall. The .eloKeyboardBannerAd modifier requests it. |
An ad carrying ctaText draws a CTA pill, in the strip’s trailing slot or on the card’s own CTA row; one without draws the disclosure chevron. The two formats then take different tap rules. The card is one tap target from edge to edge: in a transcript the card reads as a single object, so its pill labels the action rather than being a second button. The strip taps only on the pill when the creative sends a label, because the strip sits under the reader’s thumb beside the composer and an edge-to-edge target there invites accidental taps; without a label the whole row taps behind the chevron. Tracking stays on the container in both cases, so viewability is unaffected.
The strip badges the disclosure onto the creative mark’s top-trailing corner rather than leading the headline with it, falling back to the strip’s own corner for a creative with no mark; the card sets it as a line under the brand name — see EloAdDisclosure. Both marks are borderless rounded squares: 36pt on the card, 40pt on the strip. Creative artwork is usually a favicon or an apple-touch-icon, drawn for the app-icon silhouette, so a rounded square keeps the ends of a wide mark that a circular crop took off.
A CTA pill on either format can also carry an attention treatment the ad server selects: a decorative arrow after the label, a single pop of the button, or one shimmer across the pill. There is nothing to set in code, and the default is the static pill. Motion begins after one continuous second at 50% visibility and repeats every few seconds while the ad is on screen, cancels off screen, and is suppressed by Reduce Motion; the arrow widens the pill, while the pop and the shimmer are drawn and leave the layout and the tap target alone.
Tracking and accessibility
Section titled “Tracking and accessibility”The impression follows the MRC standard: at least 50% of the ad visible for one continuous second. Visibility is geometric (what clips the ad, and the window; never alpha or isHidden), scaled by the share of the ad the window’s hit test still reaches, so a sheet or alert drawn over the ad suspends the measurement and its removal starts a fresh dwell. The dwell also requires the app to be frontmost. Three kinds of cover stay undetectable, as they are for any hit-test based check: a cover that declines touches itself (allowsHitTesting(false)), a SwiftUI sibling drawn over the ad inside the same view (a ZStack or .overlay scrim has no view of its own for the hit test to return), and a cover in a separate UIWindow.
EloAdView also reports 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, 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, at most fifty taps are reported per showing. It is analytics only: never billed, and separate from click tracking. Declare it as Product Interaction in your nutrition label; see Privacy & Consent.
The view sets no accessibilityHint: VoiceOver announces the element’s label and its 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.
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-trailing corner, or on the strip’s own top-trailing corner when a creative carries no mark. Passed as sponsoredLabel to every EloAdView initializer and to .eloKeyboardBannerAd.
public struct EloAdDisclosure: Sendable, Equatable, ExpressibleByStringLiteral { public var text: String public var font: Font? public var color: Color? public var accessibilityIdentifier: String?
public init( _ text: String, font: Font? = nil, color: Color? = nil, accessibilityIdentifier: String? = nil )
public static let `default`: EloAdDisclosure // "Ad"}text- Disclosure copy, localized by you. Trimmed before rendering; blank drops the disclosure rather than drawing an empty line or capsule.
fontnilkeeps the SDK’s type: 12pt regular under the brand name on the card, 8pt medium on the strip’s badge. Setting a font pins one across both.color- The text color on the card. On the strip it fills the badge, and the copy is drawn over it in the strip background color.
nilfalls 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. accessibilityIdentifier- Names the disclosure for UI tests. It is its own element, 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 view. 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.
String literals convert implicitly, so sponsoredLabel: "Anzeige" still compiles. A String variable needs the wrap:
EloAdView( result: adResult, sponsoredLabel: EloAdDisclosure(NSLocalizedString("ad.sponsored", comment: "")))EloAdStyle
Section titled “EloAdStyle”Optional styling overrides applied via the eloAdStyle view modifier. Only non-nil fields are applied; everything else uses dark-mode-aware system defaults.
public struct EloAdStyle: Sendable, Equatable { public var cardBackground: Color? public var titleColor: Color? public var descriptionColor: Color? public var badgeColor: Color? public var callToActionBackground: Color? public var callToActionForeground: Color? public var borderColor: Color? public var borderWidth: CGFloat? public var cornerRadius: CGFloat? public var descriptionOverflow: EloAdDescriptionOverflow? public var font: EloAdFont? public var colorScheme: ColorScheme? public var chrome: EloAdChrome?
public static let `default`: EloAdStyle}
public enum EloAdFont: Sendable, Hashable { case system(design: Font.Design = .default) case custom(String)}
public enum EloAdChrome: Sendable, Hashable, CaseIterable { case standard case bare}
extension View { public func eloAdStyle(_ style: EloAdStyle) -> some View}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 background 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 accent color. Also forwarded to renderer-backed fills that draw their own CTA. callToActionForeground- CTA pill text color.
nildraws white on the default accent color, 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 on the card and 16 on the strip.
descriptionOverflow- How an over-long description behaves on the strip:
.marquee(default) scrolls it on one line;.truncatetail-truncates.niluses marquee. The card wraps its description to up to three lines and ends longer copy with an ellipsis, whatever this is. font- Typeface for the headline, description, CTA label, and disclosure badge:
.system(design:)or.custom(name)for a font registered with your app. The SDK keeps its own sizes and weights.niluses the system font. AnEloAdDisclosure.fontstill overrides the badge. colorScheme- Forces
.lightor.darkcolors on the ad and its loading skeleton.nilfollows the SwiftUI environment. chrome.standard(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.
EloAdDescriptionOverflow is an enum with cases .marquee and .truncate; marquee makes a few passes, then settles into a tail-truncated line, and falls back to a static ellipsis under Reduce Motion. It applies to the strip only.
EloAdLoadingView
Section titled “EloAdLoadingView”The shimmer skeleton EloAdView(messages:) and .eloKeyboardBannerAd show while a request is in flight. Use it directly when you run the request yourself and render the result with init(result:).
public struct EloAdLoadingView: View { public init( showCallToActionPlaceholder: Bool = false, loadingAccessibilityLabel: String = "Loading sponsored content", maxHeight: CGFloat? = nil )}showCallToActionPlaceholder- Whether the skeleton draws a CTA pill. Whether a fill draws one follows the creative, which is unknown while the request is in flight, so pass
trueonly when you know the fill renders its own CTA. loadingAccessibilityLabel- VoiceOver label for the skeleton.
maxHeight- The
maxHeightof the request the skeleton stands in for. Below 192 points it draws the strip; otherwise, or whennil, the card. This is the same rule the server’s formats follow, so the fill lands at the skeleton’s size.
EloAdDelegate
Section titled “EloAdDelegate”Lifecycle callbacks for an ad. Register with Elo.setDelegate(_:). The SDK holds the delegate weakly; all methods are dispatched on the main actor and have default no-op implementations, so you only implement what you care about.
@MainActorpublic protocol EloAdDelegate: AnyObject { func eloAdDidLoad(_ ad: EloAd) func eloAdDidFailToLoad(error: EloError) func eloAdDidTrackImpression(_ ad: EloAd) func eloAdDidReceiveClick(_ ad: EloAd)}EloError
Section titled “EloError”public enum EloError: Error, Sendable, LocalizedError { case notConfigured case networkError(URLError) case serverError(statusCode: Int, message: String?) case decodingError(String) case timeout case cancelled
public var errorCode: Int { get }}ChatMessage & MessageRole
Section titled “ChatMessage & MessageRole”Input types for Elo.loadAd(messages:).
public enum MessageRole: String, Sendable, Hashable { case user case assistant case summary}
public struct ChatMessage: Sendable, Hashable { public let role: MessageRole public let content: String public let id: String? public let createdAt: Date?
public init(role: MessageRole, content: String) public init(role: MessageRole, content: String, id: String?, createdAt: Date?)}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.
ChatMessage( role: message.isFromLocalUser ? .user : .assistant, content: message.text, id: message.id, createdAt: message.sentAt)id and createdAt 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 .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.
Use .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.
If your app already has a message type, conform it to MessageRepresentable instead of mapping by hand:
public protocol MessageRepresentable: Sendable { var role: MessageRole { get } var content: String { get } var id: String? { get } var createdAt: Date? { get } var message: ChatMessage { get }}Elo.loadAd and Elo.preloadAd both have generic overloads accepting [M] where M: MessageRepresentable. id and createdAt have default implementations that return nil, so an existing conformance keeps compiling.
Targeting Types
Section titled “Targeting Types”public struct ContextObject: Codable, Sendable, Hashable { public let type: String public let description: String
public init(type: String, description: String)}ContextObject- Screen or page metadata attached to a
loadAdrequest for richer targeting.
Diagnostics
Section titled “Diagnostics”Elo.setDebugObserver(_:capturePayloads:) reports what the SDK does as a stream of EloDebugEvent values. The SDK keeps no history, totals, or snapshot of its own: an app 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.
#if DEBUGElo.setDebugObserver { event in print(event)}#endifThe observer can be called from any thread and is never called while the SDK holds an internal lock, so it is safe to call back into Elo from it. Unlike setDelegate(_:), the closure is held strongly; pass nil to clear it. The observer is a process-level setting, independent of configure and shutdown(), so set it once, for example at app startup. It is a debugging aid, not an analytics pipeline: use EloAdDelegate for ad lifecycle callbacks in production.
public enum EloDebugEvent: Sendable { case configured(EloDebugConfiguration) case shutdown case requestPayloadCaptured(requestId: UUID, json: String) case responsePayloadCaptured(requestId: UUID, json: String) case serverRequestIdAssigned(requestId: UUID, serverRequestId: String) case adOperation(EloDebugAdOperation) case trackingAttempted( operationId: UUID, event: EloTrackingEvent, serverRequestId: String?, payloadJSON: String?, at: Date ) case trackingCompleted( operationId: UUID, event: EloTrackingEvent, outcome: EloTrackingOutcome, at: Date )}
public struct EloDebugConfiguration: Sendable, Equatable { public let publisherId: String public let adUnitId: String public let requestTimeout: TimeInterval public let validationIssues: [ValidationIssue] public let timestamp: Date}
public struct EloDebugAdOperation: Sendable, Equatable { public let requestId: UUID? public let timestamp: Date public let messageCount: Int public let contextCount: Int public let latencyMs: Int public let outcome: EloAdLoadOutcome public let format: String? public let maxHeight: Int? public let maxWidth: Int? public let ctaEnabled: Bool?}
public enum EloAdLoadOutcome: Sendable, Equatable { case loaded(eCpm: Double) case noFill(NoFillReason) case error(EloErrorCategory) // .network, .decoding, .internalState}
public enum EloTrackingEvent: Sendable, Equatable, Hashable, CaseIterable { case render case impression case click case tap case browserOpen case browserClose case viewable}
public enum EloTrackingOutcome: Sendable, Equatable { case delivered case unobservable case failed(EloTrackingFailureCategory) // .invalidURL, .network, .httpStatus(Int), .cancelled}.configuredconfigurecompleted. Every earlier event belongs to a previous configuration, so an observer should discard its history here.validationIssuesis empty for a clean integration..shutdownshutdown()ran..requestPayloadCaptured/.responsePayloadCaptured- The redacted request body and the decoded ad response, re-encoded for inspection. Sent only with
capturePayloads: true. Correlate withEloDebugAdOperation.requestId. Cache hits, disabled sessions, and transport failures produce neither. .serverRequestIdAssigned- The ad opportunity id the server assigned to a request. The ad server keys the request’s impression and click under the same id, which is also
EloAd.id. .adOperation- A
loadAdorpreloadAdfinished. Carries message and context counts (never their content), the latency, the outcome, theformatthe ad renders as (striporcard), themaxHeightandmaxWidththe request sent, and the ad’sctaEnabledpresentation value,nilwhen not sent or without a fill.requestIdisnilwhen no server request was made (a cache hit, a disabled session, or a call beforeconfigure). .trackingAttempted- A tracking operation started. Its result arrives later as
.trackingCompletedwith the sameoperationId. .trackingCompleted- A tracking operation resolved with an
EloTrackingOutcome.
EloTrackingEvent covers render, impression, and click, which EloAdView sends; .tap, every tap inside an Elo-served ad view, whether or not it opened the destination (see EloAdView); .browserOpen and .browserClose, the calls to Elo.trackBrowserOpened and Elo.trackBrowserClosed; and .viewable, 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. Tap, browser, and viewable events are analytics only and never billed; the impression is recorded separately.
EloTrackingOutcome.delivered means every required Elo tracker endpoint returned 2xx. .unobservable means tracking completed on the device but the SDK cannot observe server-side receipt; Elo-direct clicks resolve this way while client-side click delivery is disabled. .failed carries a privacy-safe category. Tracking events never include tracker URLs, request headers, or chat content.
payloadJSON on .trackingAttempted is the analytics body the operation posts. Only .tap, .browserOpen, .browserClose, and .viewable have one; render and impression are URL pings, and click posts nothing. It is nil unless the observer was registered with capturePayloads: true. It carries tap geometry and timing, the browser dwell time, or the ad view’s on-screen geometry, plus the ad opportunity id and the creative id, and never chat content.