Skip to content

Web SDK API Reference

The main SDK class. Create one instance per page/app.

import { EloSDK } from '@elo-ads/web-sdk'
const sdk = new EloSDK(config: EloConfig)

Initializes the SDK. Generates a persistent visitor ID (stored in localStorage + cookie), creates a session ID, and reads IAB consent signals from localStorage.

Returns { status: 'ready' } or { status: 'gdpr_consent_missing' } if GDPR applies but no TCF consent string is found.

Calling it again starts a new session and clears the SDK’s per-opportunity render and impression limits, so an ad shown under the new session is never suppressed by the old one. A debug observer, if set, receives an initialized event. Ads fetched before the re-init keep the bid-time identity they were requested under.

It also sends one HEAD request to {backendUrl}/v1/health, so the first fetchAds() reuses an open connection instead of setting one up. The request carries no chat content, identifiers or consent data, and no custom header, so the browser sends it without a CORS preflight.

fetchAds(params: FetchAdsParams): Promise<AdResponse>

Section titled “fetchAds(params: FetchAdsParams): Promise<AdResponse>”

Sends a bid request to the Elo ad server with the provided chat context. Throws an Error if the request fails (non-2xx response).

The request stops after 3 seconds, as on iOS and Android, and rejects with a DOMException named TimeoutError. Pass signal to stop it sooner, for example when the user sends another message before the ad arrives; it then rejects with the signal’s reason.

// Drop the previous turn's request, so a late ad can't land under the new one.
adRequest?.abort()
adRequest = new AbortController()
const res = await sdk.fetchAds({ messages, signal: adRequest.signal })

Along with the user agent, the request carries the device signals a browser gives without a permission prompt: the language, the screen size and pixel ratio, the time zone offset, and the connection type where the browser exposes it.

The returned ad carries two identities: opportunity_id is this showing (one per serve, and what the ad server keys render, impression, and click on), and ad_id is the creative, stable across showings. Correlate delivery on opportunity_id; group by ad_id.

maxHeight and maxWidth describe the space your slot has, in CSS pixels, and the server chooses the format that fits: the card needs 192px, the strip 72px, and a shorter slot gets no ad. Leave them unset for a slot in the chat transcript, which gets the card. The ad arrives with the choice as ad.presentation, and <EloAd> renders it; you never pick the format yourself. The values are rounded down to whole pixels. The same parameters exist on iOS and Android. See Ad Formats.

// A slot above the composer: the server sends the strip.
const res = await sdk.fetchAds({ messages, maxHeight: 72 })

Give each message an id and createdAt when the turn happens, and send the same values every time the turn is sent again. They tell Elo when each turn happened, and the id connects the turn to Search API calls that send it as X-Elo-Message-Id. For a summary message, use the id and time of the turn it summarizes. Without createdAt, Elo records when it received the message. An id or createdAt the SDK can’t use is left out with a console warning; the request still goes out. Only role, content, id, and createdAt are sent for each message.

The request sends dimensions rather than a display position; the server records the position from the format it chose, card for a card and banner for a strip.

setUserIdentifier(identifier: string | null): void

Section titled “setUserIdentifier(identifier: string | null): void”

Sets a publisher-supplied user identifier (e.g. your account system’s user ID). When set, it is sent as the visitor_id on ad requests and tracking pings in place of the SDK’s anonymous ID, so delivery, frequency capping, and reporting follow the user rather than the browser. Also available on the iOS and Android SDKs.

The value is held in memory only: re-set it after sign-in on each page load, and call setUserIdentifier(null) on sign-out. Passing null or a blank string clears it and restores the anonymous ID the browser had before. Values are trimmed; identifiers over 256 Unicode code points are rejected with a console warning.

setUserData(data: EloUserData | null): void

Section titled “setUserData(data: EloUserData | null): void”

Sets first-party user data (age, gender, email, and phone) to send with ad requests as an optional user object. The data is stored server-side and never joins tracking pings. It has no effect on ad selection today: the fields are recorded for upcoming targeting features and no auction or demand adapter reads them yet.

Pass contact details as they are. email and phone 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 and you don’t need to hash them yourself. Email case and surrounding whitespace are normalized for you. A phone number must carry its country code (E.164, e.g. +15551234567); without one the number is ambiguous, so it is dropped rather than guessed at.

The value is held in memory only: re-set it after sign-in on each page load, and call setUserData(null) on sign-out. Each call replaces the whole object; there is no merging. Each invalid field is dropped with a console warning while valid fields are kept. The server discards user data entirely on requests flagged COPPA or TFUA, and on any request where GDPR applies (gdpr_applies is 1), whether or not a TCF consent string is present.

trackRender(ad: AdCreative): Promise<boolean>

Section titled “trackRender(ad: AdCreative): Promise<boolean>”

Reports that ad was displayed. Only needed if you render the ad yourself — <EloAd> calls it for you on mount.

The SDK sends at most one render per ad opportunity, so this is safe to call on every render pass of a custom layout. A repeat serve of the same creative is a new opportunity and reports again. Resolves true only when the ad server confirmed the ping, and false when it was suppressed as a duplicate, refused, or failed — a failed ping releases the limit so the next call retries. Never rejects.

Tracking before init() is refused with a console warning and does not spend the opportunity’s one render, so the real ping can still follow. Also available on the iOS and Android SDKs.

trackImpression(ad: AdCreative): Promise<boolean>

Section titled “trackImpression(ad: AdCreative): Promise<boolean>”

Reports that ad met the viewable-impression standard — at least 50% visible for one continuous second. Deduped, retried, and reported exactly like trackRender(); <EloAd> calls it for you once the card qualifies.

Reports that ad was clicked, so any notices its demand source wants sent on a click go out from the reader’s browser. Only needed if you render the ad yourself — <EloAd> calls it for you, for pointer and keyboard activation alike.

This is not click tracking: the card’s click is a browser navigation straight to the ad server, which counts it there, so the SDK never sends or observes a click ping. Most ads carry no click notice and this does nothing. It returns nothing, never throws, and sends each notice once per showing however often you call it. trackTap() sends them too, so a layout already reporting taps needs no extra call.

hasTrackedImpression(ad: AdCreative): boolean

Section titled “hasTrackedImpression(ad: AdCreative): boolean”

Whether this ad opportunity’s impression is already spoken for — delivered, or in flight — so a custom layout can skip arming its own dwell timer. Goes back to false if the ping failed, since that one is retried. trackImpression() enforces the limit regardless.

trackTap(ad: AdCreative, tap: AdTap): Promise<boolean>

Section titled “trackTap(ad: AdCreative, tap: AdTap): Promise<boolean>”

Reports where inside the card a click landed. Only needed if you render the ad yourself — <EloAd> calls it for you on every click.

This is what makes a tap heatmap and a missed-call-to-action rate measurable: which region the pointer was over, where in the card it was, how big the card was, whether the click opened the destination, how long after the impression it came, and its index within the showing. Coordinates are relative to the card, never the page or the screen.

sdk.trackTap(ad, {
region: 'cta',
x: 0.82, // 0..1 across the card, 0 is the left edge
y: 0.5, // 0..1 down the card, 0 is the top edge
width: 320, // the card's size in CSS pixels
height: 88,
opened: true, // false for a click on an inert part of your layout
})

Unlike trackRender() and trackImpression() there is no dedup: every click is one event, with no limit per showing. Analytics, never billing: an event that fails on the network is retried twice, after half a second and then a second, and one the ad server rejects is not retried. A tap is dropped, without spending a tap_index, when the ad didn’t come from fetchAds() (the ad server joins the event to the ad opportunity), when the card has no measurable size, or when a coordinate isn’t a finite number.

Resolves true only when the ad server confirmed the event. Never rejects. Also available on the iOS and Android SDKs, where the SDK’s own ad views report taps the same way.

A tap with opened: true also sends the ad’s click notices, so a layout reporting taps needs no trackClick() call. A tap that opened nothing sends none.

setDebugObserver(observer, options?): void

Section titled “setDebugObserver(observer, options?): void”

Receives each debug event as it happens, for in-app diagnostics during development. observer is (event: EloDebugEvent) => void, or null to stop; options.capturePayloads also reports request and response bodies. The SDK keeps no history or counts of its own: a page that wants a recent-loads list or tracking totals records the events itself. With no observer attached the SDK builds no events. An observer that throws is ignored. Mirrors Elo.setDebugObserver on iOS and Android.

const totals = { delivered: 0, failed: 0 }
sdk.setDebugObserver((event) => {
if (event.type === 'tracking_completed' && event.event === 'impression') {
if (event.outcome.kind === 'delivered') totals.delivered += 1
else totals.failed += 1
}
})
initialized
init() finished, with its status. Earlier events belong to the previous session.
ad_operation
A fetchAds() call finished: the outcome (loaded, no_fill, or error), the latency, message and context counts, the maxHeight and maxWidth sent, and the format and ctaEnabled received.
tracking_attempted / tracking_completed
A render, impression, click, or tap started, and how it resolved: delivered, failed, or unobservable for a click, which is a browser navigation. The two share an operationId.
request_payload_captured / response_payload_captured
With capturePayloads: true only: the ad request body and the ad response.

capturePayloads also fills a tap’s payloadJSON with the body it posts. It is off by default: message content and context descriptions are removed from the request body, but it keeps consent strings and any user data set with setUserData(), including the plain email address and phone number. Use it in development only, and don’t send the events anywhere. See EloDebugEvent.

getConsentSignals(): ConsentSignals | null

Section titled “getConsentSignals(): ConsentSignals | null”

Returns the consent signals read during init(), or null if init() hasn’t been called.

Advanced / internal. Returns a base64-encoded JSON string containing visitor, session, and publisher IDs. You only need it if you render ads with a fully custom component.

getTrackingHeader(trackingUrl: string): string

Section titled “getTrackingHeader(trackingUrl: string): string”

Advanced / internal. Like getStateHeader(), but returns the state snapshot captured when the ad carrying trackingUrl was fetched (falling back to a fresh header), so tap events report the identity that was in effect at bid time. trackTap() calls this for you. Render and impression requests carry no header: their URL already holds the bid-time identity.


React component for rendering an ad in the format the server chose for it, with automatic tracking.

import { EloAd } from '@elo-ads/web-sdk'
<EloAd sdk={sdk} ad={ad} className="my-ad" style={{ maxWidth: 320 }} />
interface EloAdProps {
sdk: EloSDK // Initialized SDK instance
ad: AdCreative // Ad creative from fetchAds()
className?: string // Additional CSS class
style?: React.CSSProperties // Inline styles
sponsoredLabel?: string // The ad disclosure; defaults to "Ad"
}

The component renders the format in ad.presentation.format, the card by default. On the card one link wraps the whole ad, so every part of it clicks through. On the strip, a CTA button is the only link and the rest of the strip is inert; without a CTA the whole strip is the link. A cta_text shows as a filled capsule when the publisher’s cta_enabled is not false, on a row of its own at the card’s trailing edge or at the end of the strip; a chevron takes its place otherwise. Every click inside the ad is reported through trackTap(), including which part it hit and whether it opened the destination; a middle click on the link is reported too. A click within one second of opening the ad opens nothing and sends no click notice, so a double click opens one tab and counts one click. See Rendering Ads.


interface EloConfig {
publisherId: string // Your publisher ID from the Elo Dashboard
adUnitId: string // Your ad unit ID from the Elo Dashboard
backendUrl: string // Elo ad server URL (e.g. "https://api.withgrowl.com")
coppa?: 0 | 1 // Set to 1 if your content is directed at children under 13
tfua?: 0 | 1 // Set to 1 if tagging for users under the age of consent
}
interface EloUserData {
age?: number // Age in years, as an integer
gender?: 'male' | 'female' | 'other'
email?: string // Case and whitespace don't matter
phone?: string // E.164 format, e.g. "+15551234567"
}
interface InitResult {
status: 'ready' | 'gdpr_consent_missing'
}
interface FetchAdsParams {
messages: ChatMessage[] // Chat history for contextual matching
context?: ContextObject[] // Additional contextual signals
maxHeight?: number // CSS px the slot can give the ad; unset is unbounded
maxWidth?: number // CSS px; unset is unbounded
signal?: AbortSignal // Stops the request; it stops after 3 s regardless
}
interface ChatMessage {
role: 'user' | 'assistant' | 'summary'
content: string
id?: string // Your id for the turn, the same on every request
createdAt?: Date | string // When the turn happened; a string needs a time zone
}

Use the summary role to send a condensed recap of the conversation instead of the full history — useful for long chats or when you want to share less. Pair it with the most recent user/assistant turns.

id and createdAt are optional. Set them once, when the turn happens, and send the same values each time the turn is sent again: the id matches Search API calls that send it as X-Elo-Message-Id, and createdAt tells Elo when the turn happened. A summary takes the id and time of the turn it summarizes. An id longer than 256 Unicode code points, or a createdAt that is not a Date or an ISO 8601 string with a time zone, is left out with a console warning.

interface ContextObject {
type: string // Category of context (e.g. "topic", "product")
description: string // Free-text description
}
interface AdResponse {
request_id: string // Unique ID for this ad opportunity
ad: AdCreative | null // The matched ad, or null if no fill
ecpm: number | null // The price Elo quoted for the ad, in USD CPM (null on no fill); compare it against another network's price
}
interface AdCreative {
opportunity_id?: string // This showing — one per serve; what tracking is keyed on
ad_id: string // The creative, stable across showings
format: string // Ad format (e.g. "native")
title: string // Ad headline
description?: string // Optional ad description
image_url?: string // Optional image URL
cta_text?: string // Optional CTA button label
click_url: string // Destination URL on click
render_url: string // Tracking URL fired on render
impression_url: string // Tracking URL fired on viewable impression
trackers?: AdTracker[] // Demand-source notices this browser sends; usually absent
presentation?: AdPresentation // The format to render and the publisher's settings for it; set by fetchAds()
}
interface AdTracker {
event: 'render' | 'impression' | 'click' // the moment to send it
url: string // send as given; read nothing back
}

Some demand sources ask for the notice that an ad was displayed or clicked to come from the reader rather than from Elo’s servers, so their systems see the request as the reader’s. <EloAd> sends them for you; a layout you render yourself sends them by calling trackRender(), trackImpression() and trackClick() at the moments they name. Each URL belongs to the demand source, is sent as given with nothing read back, and goes out once per showing. Most ads carry none.

opportunity_id is stamped by fetchAds() from AdResponse.request_id — the wire returns it at the response root, not on the ad. It is optional because an AdCreative you build yourself won’t have one; the SDK then falls back to the per-opportunity render_url for its own bookkeeping.

type AdFormat = 'strip' | 'card'
interface AdPresentation {
format: AdFormat // Always set by fetchAds()
cta_enabled?: boolean // false hides the CTA button; default true
// Further render settings that <EloAd> applies; a layout of your own can ignore them.
}

ad.presentation holds the format the server chose from the maxHeight you sent and the render settings for it. <EloAd> renders all of it. A layout you render yourself should honour cta_enabled. A value from a newer server that this build does not know reads as the default, and an AdCreative without presentation renders as the card with default settings. See Ad Formats.

type AdTapRegion =
| 'thumbnail'
| 'headline'
| 'description'
| 'cta'
| 'chevron'
| 'chrome' // padding or background not covered by another region
interface AdTap {
region: AdTapRegion
x: number // 0..1 across the card, 0 is the left edge
y: number // 0..1 down the card, 0 is the top edge
width: number // the card's size in CSS pixels
height: number
opened: boolean // whether the click opened the click URL
}

Passed to trackTap() when you render the card yourself. <EloAd> builds it for you: its regions map to the .elo-ad-* class hooks, and opened is true for a click inside the ad’s link, except a repeat within one second of opening the ad, which opens nothing.

type TrackingEvent = 'render' | 'impression' | 'click' | 'tap'
// timeout and cancelled: fetchAds() passed its 3-second deadline, or your signal stopped it
type EloErrorCategory = 'network' | 'http_status' | 'decoding' | 'timeout' | 'cancelled'
type EloAdLoadOutcome =
| { kind: 'loaded'; ecpm: number | null }
| { kind: 'no_fill' }
| { kind: 'error'; category: EloErrorCategory; status?: number }
type EloTrackingOutcome =
| { kind: 'delivered' }
| { kind: 'unobservable' } // A click: a browser navigation whose response the SDK never sees
| { kind: 'failed'; category: 'network' | 'http_status'; status?: number }
interface EloDebugAdOperation {
operationId: string // Correlates the call with its payload events
timestamp: number // When the call started, in epoch milliseconds
messageCount: number
contextCount: number // Capped at 999
latencyMs: number
outcome: EloAdLoadOutcome
opportunityId: string | null // AdResponse.request_id, or null when the request failed
format: AdFormat | null // The format the ad renders as, or null without a fill
maxHeight: number | null // The dimensions sent, in whole CSS px, or null when unbounded
maxWidth: number | null
ctaEnabled: boolean | null // The ad's presentation.cta_enabled, or null when not sent or no fill
}
type EloDebugEvent =
| { type: 'initialized'; status: InitResult['status']; publisherId: string; adUnitId: string; timestamp: number }
| { type: 'ad_operation'; operation: EloDebugAdOperation }
| { type: 'request_payload_captured'; operationId: string; json: string }
| { type: 'response_payload_captured'; operationId: string; json: string }
| { type: 'tracking_attempted'; operationId: string; event: TrackingEvent; opportunityId: string | null; payloadJSON: string | null; timestamp: number }
| { type: 'tracking_completed'; operationId: string; event: TrackingEvent; outcome: EloTrackingOutcome; timestamp: number }
interface DebugObserverOptions {
capturePayloads?: boolean // Also report each ad request and response body; off by default
}

Passed to the observer set with setDebugObserver(). Message content and context descriptions are never carried, only their counts. payloadJSON on tracking_attempted is the body a tap posts, with capturePayloads only; render and impression post no body, and a click sends nothing the SDK can see. A failed render or impression ping is retriable: the SDK releases the per-opportunity limit so the next mount or dwell sends again. A tap event that fails on the network is retried twice before its one tracking_completed reports the outcome; one the ad server rejects is not retried.

interface ConsentSignals {
coppa: number // COPPA flag (0 or 1)
tfua: number // TFUA flag (0 or 1)
gdpr_applies: number | null // IAB TCF gdprApplies value, or null if not set
tcf_string: string | null // IAB TCF v2.0 consent string
addtl_consent: string | null // Additional consent string
gpp_string: string | null // IAB GPP consent string
gpp_sid: string | null // GPP section IDs
}