JavaScript SDK for integrating with an Optable Data Connectivity Node (DCN) from a web site or web application.
- Installing
- Versioning
- Domains and Cookies
- Using the NPM module
- Initialization Configuration (
InitConfig) - Usage Example
- Using a script tag
- Integrating PrebidJS analytics
- Integrating GAM360
- Integrating Prebid
- Identifying visitors arriving from Email newsletters
- Identify and tokenize (hashed email)
- Passport and Visitor ID
- Optable Identity System (OIS)
- QA and debug flags
- Multi-Node Targeting Resolver
- Geo-routing
- Bot detection
- Command queue
- EID cache merge
- UID2 refresh
- Prebid pubProvidedId delivery
- Static mappings
- ID5 resolution
- Source check
- A/B test assignment
- Traffic source
- GAM signal enrichment key-value
- Other SDK methods
- Demo Pages
The Optable web SDK can be installed as a ES6 compatible npm module using package managers such as pnpm, paired with module bundlers such as webpack or browserify, or can be loaded on a webpage directly by referencing a release build from the page HTML via a <script> tag.
⚠️ CORS Configuration: Regardless of how you install the SDK, make sure that the Allowed HTTP Origins setting in the Optable DCN that you are integrating with contains the URL(s) of any web site(s) where the SDK is being used, otherwise your browser may block communication with the DCN.
If you're building a web application or want to bundle the SDK functionality with your own JavaScript, then using a package manager like pnpm or npm is the recommended installation method. It pairs nicely with module bundlers such as webpack or browserify and exports types for applications using the typescript language and type checker. To use it simply install the package:
# latest stable release (using pnpm):
pnpm install @optable/web-sdk
# or using npm:
npm install @optable/web-sdkAnd then simply import and use the OptableSDK class as shown in the Usage section below.
For simple integrations from your web site, you can load the SDK built for the browser from Optable's CDN via a HTML script tag. In production it's advised to lock your SDK bundle to a specific major version identified by vX or a specific minor version with vX.Y, while in development you may want to experiment with latest.
E.g. in development use the following in the <head> block of your HTML page:
<!-- Latest version for development -->
<script async src="https://cdn.optable.co/web-sdk/latest/sdk.js"></script>Or in production:
<!-- v0 in production -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>Note the presence of the async attribute, which instructs browsers to load the library asynchronously and not block the page from rendering.
The SDK follows Semantic Versioning conventions. You can therefore expect that there will not be any breaking API changes if you are tracking a particular major version.
By default, the Optable SDK makes use of a secure HTTP-only first-party browser cookie in order to anonymously identify browsers via a visitor ID, within the context of any web sites sharing an effective top-level domain plus one (eTLD+1) with the configured DCN host.
For example, if your website runs at www.customer.com or customer.com, then ideally your DCN will be configured to run at dcn.customer.com, and will read/write a first-party cookie at customer.com. The contents of the cookie will not be accessible to any third-party scripts. Finally, the cookie will have the SameSite=Laxattribute so that it is available on the first visit.
⚠️ Optable Visitor ID Scope: The visitor ID configured by the Optable DCN will be unique to a browser only within the top-level domain that the DCN shares with the calling web site.
In cases where it is not practical or possible to configure your DCN to run on the same effective top-level domain plus one (eTLD+1) as your website(s), then the default cookie-based transport that the SDK depends on will not work. Instead, you can configure the SDK to use browser LocalStorage. To switch to the LocalStorage based configuration, simply set the optional cookies parameter to false when creating your SDK instance. For example:
import OptableSDK from "@optable/web-sdk";
const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site", cookies: false });Note that the default is cookies: true and will be inferred if you do not specify the cookies parameter at all.
When creating an instance of OptableSDK, you can pass an InitConfig object to customize its behavior. Below are the available configuration keys and their descriptions:
-
site(string) The identifier (slug) of Javascript SDK source. This must match a configured site in the Optable DCN. Must have properly configureAllowed HTTP Origins. -
host(string) The hostname of the Optable DCN to which the SDK will connect. All API requests will be directed here.
-
node(string) If supported by the DCN host, specify the API node for SDK requests. Used in multi-node environments. -
cookies(boolean, default:true) Iftrue, enables the use of browser cookies for storage. -
initPassport(boolean, default:true) Iftrue, initializes the user passport (identity mechanism) upon SDK load. -
initTargeting(boolean, default:false) Iftrue, the SDK will automatically perform a targeting request during initialization and store the response in cache. This ensures the cache is populated with the most up-to-date targeting data as soon as the SDK is loaded. -
pageContext(PageContextConfig | boolean, default:undefined) When set, enables page context extraction for contextual intelligence. Set totrueto use defaults, or pass aPageContextConfigobject to customize what is extracted:includeHtml(boolean),contentSelector(CSS selector for the main content),maxContentLength(default5000) andmaxHtmlLength(default50000). Extracted context is automatically attached to the firstwitness()call that uses{ includeContext: true }. -
initContextual(boolean | (response: ContextualSegmentsResponse) => void, default:false) Iftrue, the SDK will automatically fire apageviewwitness event with full page context during initialization, and also callctxSegments()to fetch contextual segments for the current page, caching the result on the instance for later use viactxTargetingKeyValues(). This is the recommended way to enable contextual pageview tracking and contextual targeting without writing custom code. ImpliespageContext: truewhen nopageContextis explicitly configured. When set to a callback function, the SDK does everythingtruedoes and invokes the callback with theContextualSegmentsResponseonce it resolves — useful for chaining an ad-server load (e.g. GAM) on the contextual response without making a secondctxSegments()call. The callback is not invoked if the request fails. -
consent(InitConsent) Defines the consent settings for data collection and processing:{ cmpapi?: CMPApiConfig; static?: Consent }. Passcmpapito read consent from the page's CMP (TCF/GPP), orstaticto supply consent you already collected;staticwins if both are set. When omitted, the SDK assumes all-permissive consent with no applicable regulation. -
readOnly(boolean, default:false) When set totrue, addsro=trueto every request so the DCN does not modify data while still answering API queries. The SDK itself does not block any calls client-side. -
optableCacheTargeting(string, defaults:optable-cache:targeting) Local storage cache key used to store latest targeting response. -
ois(boolean, default:false) When set totrue, participates in the Optable Identity System: the SDK stores the derived OIS ID the DCN returns on theX-Optable-OIDresponse header and replays it on subsequent requests, so the DCN recognizes the browser instead of deriving a new identity each visit. Requires a DCN node with OIS ID derivation enabled and device access consent, so it is a no-op otherwise. TheOPTABLE_OIDcookie identity is separate and needs no configuration. -
timeout(string, default:undefined) Timeout hint sent to the DCN on every request, with a unit (e.g."100ms","2s"). The server tries to answer within it; targeting may return a partial response. -
sessionID(string, default: random) Session identifier sent assidon every request. Defaults to a random value generated per SDK instance. -
skipEnrichment(boolean, default:undefined) Whentrue, sendsskip_enrichment=trueso the DCN skips enrichment for these requests. -
additionalTargetingSignals({ ref?: boolean }, default:undefined) Withref: true, targeting calls also send the current page URL (protocol, host and path, without the query string) asref. -
abTests(ABTestConfig[], default:undefined) Server-side A/B tests for targeting. Each entry is{ id, trafficPercentage, matcher_override?, skipMatchers?, skipResolvers? }. On each targeting call one test is picked at random according totrafficPercentage(percentages must sum to 100 or less, otherwise no test is applied) and itsidis sent asab_test_id, along with any matcher overrides and skipped matchers/resolvers. For a sticky, per-visitor split on the client, seesetupABinlib/addons/abTestAssignment.md. -
legacyHostCache(string, default:undefined) The previous DCN host, when migrating hosts. The SDK also reads the passport stored under that host so visitors keep their identity across the switch. -
mockedIP(string, default:undefined) For testing only: sent as theX-Forwarded-Forheader so the DCN treats requests as coming from that IP. -
insecure(boolean, default:false) For local testing only: useshttpinstead ofhttps. Do not use in production.
These configurations allow fine-tuned control over how the OptableSDK interacts with the Optable DCN, ensuring compatibility with different environments and privacy settings.
To configure an instance of OptableSDK integrating with an Optable DCN running at hostname dcn.customer.com, from a configured web site origin identified by slug my-site, you simply create an instance of the OptableSDK class exported by the @optable/web-sdk module:
import OptableSDK from "@optable/web-sdk";
const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site" });You can then call various SDK APIs on the instance as shown in the examples below. It is also possible to configure multiple instances of OptableSDK in order to connect to different DCNs or reference multiple site slugs.
- All SDK communication with Optable DCNs is done over TLS to ensure data security.
- The
consentoption allows compliance with privacy regulations by defining explicit data collection settings.
To associate a user's browser with an authenticated identifier such as an Email address, optionally linked with other identifiers, such as your own vendor, publisher, or site-level PPID, you can call the identify API as follows:
const onSuccess = () => console.log("Identify API success!");
const onFailure = (err) => console.warn(`Identify API error: ${err.message}`);
const emailID = OptableSDK.eid("some.email@address.com");
// Identify with Email ID (eid):
sdk.identify(emailID).then(onSuccess).catch(onFailure);
// You can optionally link it with your own PPID in the same DCN identification call,
// simply pass a second argument to identify(). A custom PPID value can be sent to identify()
// after it is prepared with the OptableSDK.cid() helper:
const ppid = OptableSDK.cid("some.ppid");
sdk.identify(emailID, ppid).then(onSuccess).catch(onFailure);The identify() method will asynchronously connect to the configured DCN and send IDs for resolution.
⚠️ Client-Side Email Hashing: TheOptableSDK.eid()helper will compute the SHA-256 hash of the Email address on the client-side and send the hashed value to the DCN. The Email address is not sent by the browser in plain text.
The frequency of invocation of identify is up to you, however for optimal identity resolution we recommended to call the identify() method on your OptableSDK instance on each page load while the user is authenticated, or periodically such as for example once every 15 to 60 minutes while the user is authenticated and actively using your site.
To associate key value traits with a user's browser, for eventual audience assembly, you can call the profile API as follows:
const onSuccess = () => console.log("Profile API success!");
const onFailure = (err) => console.warn(`Profile API error: ${err.message}`);
const visitorTraits = {
gender: "M",
age: 44,
favColor: "blue",
hasAccount: true,
};
sdk.profile(visitorTraits).then(onSuccess).catch(onFailure);The specified visitor traits are associated with the user's browser and can be matched during audience assembly.
Note that visitor traits are key value pairs and have type ProfileTraits:
type ProfileTraits = {
[key: string]: string | number | boolean;
};You can also override the main identifier (replacing the Optable Visitor ID) as the second argument of the function. The third argument is to provide additional identifier(s) that you want to associate to that profile.
const onSuccess = () => console.log("Profile API success!");
const onFailure = (err) => console.warn(`Profile API error: ${err.message}`);
const visitorTraits = {
gender: "M",
age: 44,
favColor: "blue",
hasAccount: true,
};
const emailID = OptableSDK.eid("some.email@address.com");
const additionalIDs = [];
additionalIDs.push(OptableSDK.cid("id1"));
additionalIDs.push(OptableSDK.cid("id2", 2)); // variant 0-19, prefixes the ID with "c2:"
sdk.profile(visitorTraits, emailID, additionalIDs).then(onSuccess).catch(onFailure);To get the targeting information associated by the configured DCN with the user's browser in real-time, you can call the targeting API as follows:
sdk
.targeting()
.then((response) => {
console.log("Audience targeting:", response.audience);
console.log("User targeting:", response.user);
})
.catch((err) => console.warn(`Targeting API Error: ${err.message}`));The targeting() function accepts different parameter formats:
// Uses the default passport identifier
sdk.targeting();
// Uses a specific identifier
sdk.targeting("some_identifier");To target multiple identifiers, use the object parameter format:
// Target multiple identifiers
sdk
.targeting({ ids: ["identifier1", "identifier2", "identifier3"] })
.then((response) => {
console.log("Multi-targeting response:", response);
})
.catch((err) => console.warn(`Targeting API Error: ${err.message}`));The targeting function accepts the following parameter types:
// targeting(input?: string | TargetingRequest)
type TargetingRequest = {
ids: string[]; // identifiers, sent as `id`
hids: string[]; // hint identifiers, sent as `hid`
};
// Examples:
sdk.targeting(); // Uses default "__passport__"
sdk.targeting("some_id"); // Single identifier
sdk.targeting({ ids: ["id1", "id2"], hids: [] }); // Multiple identifiers
sdk.targeting({ ids: ["id1"], hids: ["hint1"] }); // Identifiers with hint identifiersIn TypeScript both ids and hids are required; pass an empty array for the one you don't use. In JavaScript a missing field defaults to [].
On success, the resulting targeting data is typically sent as part of a subsequent ad call. Therefore we recommend that you either call targeting() before each ad call, or in parallel periodically, caching the resulting targeting data which you then provide in ad calls.
The targeting API will automatically cache resulting key value data in client storage on success. You can subsequently retrieve the cached key value data as follows:
const cachedTargetingData = sdk.targetingFromCache();
if (cachedTargetingData) {
console.log("Audience targeting:", cachedTargetingData.audience);
console.log("User targeting:", cachedTargetingData.user);
}You can also clear the locally cached targeting data:
sdk.targetingClearCache();Note that both targetingFromCache() and targetingClearCache() are synchronous.
To send real-time event data from the user's browser to the DCN for eventual audience assembly, you can call the witness API as follows:
const onSuccess = () => console.log("Witness API success!");
const onFailure = (err) => console.warn(`Witness API error: ${err.message}`);
const eventProperties = {
property_one: "some_value",
property_two: 123,
property_three: false,
};
sdk.witness("event_type_here", eventProperties).then(onSuccess).catch(onFailure);The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly.
Note that event properties are key value pairs and have type WitnessProperties:
type WitnessProperties = {
[key: string]: string | number | boolean | unknown[] | null | { [key: string]: unknown };
};The SDK can automatically fire a pageview witness event with full page context on initialization. This sends semantic content from the page (title, description, keywords, headings, canonical URL, Open Graph tags, and body text) to the DCN for contextual audience assembly.
Set initContextual: true in your SDK config. The SDK fires the pageview event once during initialization (after the passport is initialized, when initPassport is on), with no additional code required:
const sdk = new OptableSDK({
host: "dcn.customer.com",
site: "my-site",
initContextual: true,
});You can combine this with pageContext to customize what content is extracted:
const sdk = new OptableSDK({
host: "dcn.customer.com",
site: "my-site",
initContextual: true,
pageContext: {
contentSelector: "article",
maxContentLength: 3000,
},
});Enable pageContext in your config and call witness() with { includeContext: true } yourself. Context is attached once per SDK instance (subsequent calls with includeContext: true send no context until resetContext() is called):
const sdk = new OptableSDK({
host: "dcn.customer.com",
site: "my-site",
pageContext: true,
});
// Fire a pageview with URL property and full page context attached
const url = `${window.location.hostname}${window.location.pathname}`;
sdk.witness("pageview", { url }, { includeContext: true });To reset the context (e.g. on SPA navigation), call sdk.resetContext() before the next witness() call.
In addition to pageview tracking, the SDK can classify a page URL against one or more contextual taxonomies (such as the IAB Content Taxonomy) and use the result for ad targeting. Call ctxSegments() to fetch the contextual classifications for a URL:
// Classify the current page (defaults to window.location.href):
const response = await sdk.ctxSegments();
// Or classify an explicit URL:
const response = await sdk.ctxSegments("https://example.com/article");The response has the shape:
type ContextualSegmentsResponse = {
classifications: {
categories: { id: string; name: string; score: number; taxonomy: string }[];
keywords: { keyword: string; prominence: number /* 1 = most prominent */ }[];
brandSafety: {
assessed: boolean;
categories: { name: string; riskLevel: "low" | "medium" | "high" | "floor" | "not_assessed" | "" }[];
};
sentiment:
| { scored: false }
| { scored: true; id: "positive" | "neutral" | "negative"; name: string; score: number };
emotions: {
id: "joy" | "trust" | "fear" | "surprise" | "sadness" | "disgust" | "anger" | "anticipation";
name: string;
score: number;
}[];
entities: {
name: string;
kind: "person" | "organization" | "product" | "location" | "event" | "work_of_art";
score: number;
}[];
};
};classifications groups results by classification method; the DCN populates only the methods it has enabled. categories and keywords are empty when the DCN never classified the page or does not run that classifier.
Note: The requested URL must already have been classified by the DCN.
Each successful call to ctxSegments() caches its response on the SDK instance (calling it again refreshes the cache; a failed call leaves the previous response in place). When initContextual: true, the SDK calls ctxSegments() for you during initialization, so the cache is populated automatically.
ctxBrandSafety() reads the brand-safety classifications off the cached ctxSegments() response, as { assessed, categories }. The categories and the risk tiers follow the Brand Safety Floor + Suitability Framework, which is where the category list and the term floor come from.
Note: Only the page's text is assessed. Embedded media, such as images, video and audio, is not, so a page can come back with nothing flagged and still carry unsafe media. Treat the result as a signal about what the page says, not about everything a visitor sees.
Read assessed before categories. An empty categories list means two opposite things depending on it:
assessed |
categories |
Meaning |
|---|---|---|
true |
empty | An assessment ran and flagged nothing. |
true |
non-empty | An assessment ran: each entry is a category it flagged, at its riskLevel, or a not_assessed category the pass did not cover. Assessed categories with no finding are omitted. |
false |
always empty | Nothing is known about this page. The DCN never classified it, the read failed, or this DCN does not run the brand-safety classifier at all. |
Three more things to keep in mind:
flooris the most severe tier, not a baseline. The order islow<medium<high<floor,floorbeing the framework's brand-safety floor, below which inventory should not monetize.not_assessedis not a severity at all; it is the entry's own state.- A page assessed clean is not a clearance. The taxonomy enumerates risks, so the classifier can report that a page matches one but never that it is free of them. Neither the response nor these helpers can express "safe".
riskLevelcan be"". Only when the DCN stored a tier it could not resolve, which is a defect on its side rather than a state of the page. It is in the type because the wire can carry it, so an exhaustiveswitchhas to handle it; treat it as "no usable tier".ctxMaxRiskLevel()already ignores it.
ctxMaxRiskLevel() returns the most severe tier flagged on the page, or null when nothing is flagged, which is convenient for a single gate:
if (sdk.ctxMaxRiskLevel() === "floor") {
// Do not monetize this page.
}null comes back in two different situations: an assessment ran and flagged nothing, and nothing is known about the page at all. A gate that must tell those apart reads ctxBrandSafety().assessed as well.
ctxSentiment() reads the page's sentiment (its polarity) off the cached ctxSegments() response:
const sentiment = sdk.ctxSentiment();
if (sentiment.scored) {
// sentiment.id is "positive", "neutral" or "negative"; sentiment.score is from -1.0 to 1.0.
}Read scored first. When it is false there is no sentiment for the page: the DCN never classified it, does not run the sentiment classifier, or found nothing to score. id, name and score are then absent, so a missing score is never mistaken for a neutral one.
The label in id is derived by the DCN from the score. Use it as served rather than bucketing score yourself, so your targeting follows the DCN's thresholds.
ctxEmotions() reads the emotions the page evokes off the cached ctxSegments() response, as a list of { id, name, score }:
sdk.ctxEmotions();
// => [{ id: "joy", name: "Joy", score: 0.9 }, { id: "sadness", name: "Sadness", score: 0.7 }]id is one of the eight Plutchik emotions: joy, trust, fear, surprise, sadness, disgust, anger or anticipation. score is from 0.0 to 1.0, and the DCN serves the list highest score first. It is multi-label: a page can evoke several emotions at once, opposites included (a bittersweet story can carry both joy and sadness).
The list is empty when the page has no emotion: the DCN never classified it, does not run the emotion classifier, or nothing reached its cutoff. The DCN applies that cutoff before serving, so use the list as served rather than filtering on score yourself.
ctxEntities() reads the named entities the page is about off the cached ctxSegments() response, as a list of { name, kind, score }:
sdk.ctxEntities();
// => [{ name: "LeBron James", kind: "person", score: 0.9 }, { name: "Los Angeles Lakers", kind: "organization", score: 0.8 }]kind is one of person, organization, product, location, event or work_of_art. score is the entity's salience from 0.0 to 1.0, and the DCN serves the list highest score first. The list is empty when the page has no entity: the DCN never classified it or does not run the entity classifier.
ctxTargetingKeyValues(taxonomyKeys?, options?) reads the cached ctxSegments() response and builds a Record<string, string[]>, ready to pass to an ad server such as Google Ad Manager via googletag.pubads().setTargeting(). It emits category ids grouped by taxonomy, plus, by default, the page's keywords under ctx_kw, its brand-safety tier under ctx_bs_max, its sentiment label under ctx_sentiment, its emotions under ctx_emotion and its entities under ctx_ent:
sdk.ctxTargetingKeyValues();
// => {
// "iab_ct_3_1": ["53", "91", "58", "115", "90", "52"],
// "ctx_kw": ["advertising", "programmatic", "ad tech"],
// "ctx_bs_max": ["no_flags"],
// "ctx_sentiment": ["positive"],
// "ctx_emotion": ["joy", "anticipation"],
// "ctx_ent": ["lebron james", "los angeles lakers"]
// }The examples below show only the keys each option affects; ... stands for the other default keys.
Pass a taxonomyKeys map to rename category keys. Only taxonomies present in the map are emitted (filter + rename), which is useful when you only want to set keys you have configured in your ad server:
sdk.ctxTargetingKeyValues({ iab_ct_3_1: "foo" });
// => { "foo": ["53", ...], "ctx_kw": ["advertising", "programmatic", "ad tech"], "ctx_bs_max": ["no_flags"], ... }Keyword values are ordered by prominence (most prominent first), capped to the top 10, and sanitized to GAM's value rules (lowercased, reserved characters stripped, truncated to the 40-character value limit). Use the options argument to change the keyword key (keywordKey), change the cap (maxKeywords), or opt out of keyword key-values by passing an empty keywordKey:
// Rename the keyword key and emit only the top 5 keywords:
sdk.ctxTargetingKeyValues({ iab_ct_3_1: "foo" }, { keywordKey: "kw", maxKeywords: 5 });
// => { "foo": ["53", ...], "kw": ["advertising", "programmatic", "ad tech", "marketing", "audience targeting"], ... }
// Opt out of keyword key-values entirely:
sdk.ctxTargetingKeyValues(undefined, { keywordKey: "" });
// => { "iab_ct_3_1": ["53", "91", "58", "115", "90", "52"], "ctx_bs_max": ["no_flags"], ... }Brand safety is emitted by default under ctx_bs_max, carrying the page's most severe flagged tier. It is the same text-only assessment described above, so a line item keyed on it is gating on the page's words and not on its embedded media. Use brandSafetyKey to rename it, or pass an empty brandSafetyKey to opt out, exactly as keywordKey works:
// Rename the brand-safety key:
sdk.ctxTargetingKeyValues(undefined, { brandSafetyKey: "bs" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "bs": ["floor"], ... }
// Opt out of the brand-safety key-value entirely:
sdk.ctxTargetingKeyValues(undefined, { brandSafetyKey: "" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], ... }A value is emitted for every state rather than the key being dropped when nothing is flagged:
| State | Value |
|---|---|
| A tier was flagged | The most severe one: low, medium, high or floor |
| Assessed, nothing flagged | no_flags |
| Nothing is known about the page | not_assessed |
The two sentinels exist so a line item can tell them apart. If the key were simply absent when nothing was flagged, "we never looked" and "we looked and flagged nothing" would look identical in GAM, and you could not exclude unassessed inventory without also excluding clean inventory. Note that no_flags is deliberately not named safe: a pass with no finding is not a clearance.
not_assessed is spelled the same as the riskLevel a category carries when the assessment did not cover it. That is deliberate: one word for one idea, "nothing is known here", at two scopes. The scopes cannot be confused, because ctx_bs_max only ever carries a page-level answer.
One difference from keywordKey is worth knowing before you ship: keywords are dropped when the DCN produced none, whereas ctx_bs_max is always present unless you disable it. A DCN that does not run the brand-safety classifier therefore adds ctx_bs_max=not_assessed to every ad request. That is the intended behavior, since it is a state a line item may want to target, but if you do not use brand safety at all, pass an empty brandSafetyKey to keep it out of the request.
The page's sentiment label is emitted by default under ctx_sentiment, as positive, neutral or negative. Like keywords, the key is omitted when the page has no sentiment. The score is never emitted: GAM matches values exactly, so a raw score cannot be range-targeted. Use sentimentKey to rename the key, or pass an empty sentimentKey to opt out:
sdk.ctxTargetingKeyValues(undefined, { sentimentKey: "mood" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "ctx_bs_max": ["no_flags"], "mood": ["positive"], ... }
sdk.ctxTargetingKeyValues(undefined, { sentimentKey: "" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "ctx_bs_max": ["no_flags"], ... }Every one of the page's emotions is emitted by default under ctx_emotion, as a multi-value key (e.g. ctx_emotion=[joy,sadness]). Like keywords, the key is omitted when the page has no emotion, and scores are never emitted. Use emotionKey to rename the key, or pass an empty emotionKey to opt out:
sdk.ctxTargetingKeyValues(undefined, { emotionKey: "feel" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "ctx_bs_max": ["no_flags"], "feel": ["joy", "sadness"], ... }
sdk.ctxTargetingKeyValues(undefined, { emotionKey: "" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "ctx_bs_max": ["no_flags"], ... }The names of the page's entities are emitted by default under ctx_ent, as a multi-value key ordered by score (highest first), capped to the top 10, and sanitized like keywords (so Beyoncé Knowles becomes beyoncé knowles). Like keywords, the key is omitted when the page has no entity. Kinds and scores are never emitted. Use entityKey to rename the key, maxEntities to change the cap, or pass an empty entityKey to opt out:
sdk.ctxTargetingKeyValues(undefined, { entityKey: "who", maxEntities: 1 });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "ctx_bs_max": ["no_flags"], "who": ["lebron james"] }
sdk.ctxTargetingKeyValues(undefined, { entityKey: "" });
// => { "iab_ct_3_1": ["53", ...], "ctx_kw": [...], "ctx_bs_max": ["no_flags"] }A typical Google Ad Manager activation uses a loadGAM() helper:
// Helper to load GAM ads with optional targeting data:
var loadGAM = function (tdata = {}) {
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function () {
for (const [key, values] of Object.entries(tdata)) {
googletag.pubads().setTargeting(key, values);
}
googletag.pubads().refresh();
});
};Because ctxTargetingKeyValues() reads the cached response, the instance should be initialized with initContextual: true so the segments are fetched during initialization and the cache is likely populated by the time loadGAM() runs:
loadGAM(optable.instance.ctxTargetingKeyValues());If you want loadGAM() to run as soon as the contextual segments arrive — without making a second ctxSegments() call — pass a callback to initContextual. The SDK fires the contextual request automatically during initialization and invokes the callback with the response, populating the cache before ctxTargetingKeyValues() reads from it:
const sdk = new OptableSDK({
host: "dcn.customer.com",
site: "my-site",
initContextual: function (response) {
loadGAM(sdk.ctxTargetingKeyValues());
},
});If you are not using initContextual at all, fetch the segments explicitly and call loadGAM() once ctxSegments() resolves (falling back to an untargeted load on error):
optable.cmd.push(function () {
optable.instance
.ctxSegments()
.then(() => loadGAM(optable.instance.ctxTargetingKeyValues()))
.catch((err) => {
loadGAM();
});
});You can also rename and allow-list the GAM keys by passing a taxonomyKeys map to ctxTargetingKeyValues() (only the taxonomies present in the map are emitted):
// Emit only the "iab_ct_3_1" taxonomy, under the GAM key "ctx_iab":
loadGAM(optable.instance.ctxTargetingKeyValues({ iab_ct_3_1: "ctx_iab" }));When GAM is the only consumer, setContextualTargetingInGAM(sdk, taxonomyKeys?, options?, url?) wraps the fetch-convert-push sequence into one call: it fetches the segments, converts them with ctxTargetingKeyValues() (forwarding taxonomyKeys and options), and queues a googletag.pubads().setTargeting() call per key — creating the googletag command queue if the page has none yet. Pass url to classify a route other than the current location, as in an SPA:
import { setContextualTargetingInGAM } from "@optable/web-sdk/lib/dist/addons/gpt";
await setContextualTargetingInGAM(sdk, { iab_ct_3_1: "ctx_iab" });Nothing is queued when the page yields no key-values, and the call never rejects: a failed classification is logged under optableDebug and ads load untargeted rather than an unhandled rejection reaching a page that fired this without awaiting. For the full behaviour, including the extra request when the SDK is configured with initContextual, see the GPT addon README.
For each SDK release, a webpack-generated browser bundle targeting the browsers list described by pnpm dlx browserslist "> 0.25%, not dead" can be loaded on a website via a script tag.
As described in the Installation section above, the recommended way to load the SDK via script tag is asynchronously using the async attribute, to avoid blocking page rendering.
If you want to avoid manually instantiating the SDK, you can define the instance_config before loading the SDK bundle. When the script loads, it will automatically initialize the SDK using this configuration and assign the instance to window.optable.instance.
<!-- Define configuration before loading the SDK -->
<script>
window.optable = { cmd: [], instance_config: { host: "dcn.customer.com", site: "my-site" } };
</script>
<!-- Asynchronously load the SDK -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<!-- Optionally identify a client-side user after the page loads -->
<script>
window.addEventListener("DOMContentLoaded", () => {
optable.cmd.push(() => {
const emailInput = document.getElementById("email");
optable.instance.identify(optable.SDK.eid(emailInput.value)).then(() => {
console.log("Identify API Success!");
});
});
});
</script>You can also manually initialize the SDK using the cmd queue. This approach is useful if you prefer full control or are loading the config dynamically.
<!-- Asynchronously load the SDK as early as possible: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<!-- Later in the page: -->
<script>
// Setup stub that will get replaced once the SDK get loaded
window.optable = window.optable || { cmd: [] };
optable.cmd.push(() => {
// At this point optable.SDK is available and can be used to create a new sdk instance.
// That instance can be stored anywhere for later referencing.
// One option is to keep it within the global optable object space.
optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
});
// Now configure DOM content loaded event listener to dispatch identify() API:
window.addEventListener("DOMContentLoaded", (event) => {
optable.cmd.push(() => {
// Fetch input on document load
const emailInput = document.getElementById("email");
optable.instance.identify(optable.SDK.eid(emailInput.value)).then(() => console.log("Identify API Success!"));
});
});
</script>
<input type="text" id="email" value="some.email@address.com" />The OptablePrebidAnalytics addon hooks into Prebid.js auction events (auctionInit, auctionEnd, bidWon, bidTimeout and tcf2Enforcement) and sends auction analytics to the Optable DCN via the witness API. It reports per-bidder EID coverage, bid outcomes, and optional custom key-value pairs, enabling you to measure the impact of Optable targeting on your Prebid auctions. Auctions still pending when the page is hidden are flushed with sendBeacon.
The tenant reported in the analytics payload is the SDK instance's node, so create the instance with node set; without it the tenant is reported as unknown.
When the SDK is loaded via a <script> tag, OptablePrebidAnalytics is available as window.optable.OptablePrebidAnalytics.
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<!-- Prebid.js async load: -->
<script async src="prebid.js"></script>
<script>
window.optable = window.optable || { cmd: [] };
window.pbjs = window.pbjs || { que: [] };
optable.cmd.push(function () {
optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site", node: "my-node" });
const analytics = new optable.OptablePrebidAnalytics(optable.instance, {
analytics: true,
});
analytics.hookIntoPrebid(window.pbjs);
});
</script>When using the SDK as an NPM package, import OptablePrebidAnalytics directly from the addon module:
import OptableSDK from "@optable/web-sdk";
import OptablePrebidAnalytics from "@optable/web-sdk/lib/dist/addons/prebid/analytics";
const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site", node: "my-node" });
const analytics = new OptablePrebidAnalytics(sdk, {
analytics: true,
});
analytics.hookIntoPrebid(window.pbjs);For extended configuration options such as sampling, debug mode, and custom analytics data, see the Prebid analytics addon README.
The Optable Web SDK can fetch targeting data from a DCN and map it to be sent to Google Ad Manager 360 ad server account for real-time targeting. It's also capable of intercepting advertising events from the Google Publisher Tag and logging them to a DCN via the witness API.
The GPT methods (installGPTEventListeners() and installGPTSecureSignals()) are included in the script-tag bundle. With the NPM package, import the addon once so they are added to OptableSDK:
import "@optable/web-sdk/lib/dist/addons/gpt";Loading the Optable SDK via a script tag on a web page which also uses the Google Publisher Tag, we can further extend the targeting example above to show an integration with a Google Ad Manager 360 ad server account.
It's suggested to load the GAM banner view with an ad even when the call to your DCN targeting() method raises an exception, as shown in the example below:
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<!-- Google Publisher Tag (GPT) async load: -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<!-- Optable SDK, GPT, and targeting data initialization: -->
<script>
window.optable = window.optable || { cmd: [] };
window.googletag = window.googletag || { cmd: [] };
// Init Optable SDK via command:
optable.cmd.push(function () {
optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
});
// Init GPT and disable initial ad load so that we can load targeting data first:
googletag.cmd.push(() => {
adSlot = googletag
.defineSlot(...)
.addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.pubads().disableInitialLoad();
googletag.enableServices();
});
</script>
<!-- Placeholder DIV for adSlot... referenced by googletag.defineSlot() above: -->
<div id="div-gpt-ad-12345-0"></div>
<script>
// Helper to load GAM ads with optional targeting data:
var loadGAM = function (tdata = {}) {
// Sets up page-level targeting in GAM360 GPT:
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function () {
for (const [key, values] of Object.entries(tdata)) {
googletag.pubads().setTargeting(key, values);
}
// Explicitly calls refresh() on googletag:
googletag.pubads().refresh();
});
};
// Call Optable DCN for targeting data and setup GPT page-level targeting, then
// explicitly refresh GPT ads.
//
// NOTE: We load and refresh GPT ads without targeting data when there is an exception,
// so that GAM ads are always loaded.
optable.cmd.push(function () {
optable.instance
.targetingKeyValues()
.then(loadGAM)
.catch((err) => {
loadGAM();
});
});
googletag.cmd.push(() => {
googletag.display(adSlot);
});
</script>Note the use of googletag.pubads().disableInitialLoad() in the above example. This will disable GAM ads from loading until the call to googletag.pubads().refresh() from the loadGAM() function.
It's also possible to avoid disabling of the initial ad load by using the SDK's targetingKeyValuesFromCache() method instead as in the following example:
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<!-- Google Publisher Tag (GPT) async load: -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<!-- Optable SDK, GPT, and targeting data initialization: -->
<script>
window.optable = window.optable || { cmd: [] };
window.googletag = window.googletag || { cmd: [] };
// Init Optable SDK via command:
optable.cmd.push(function () {
optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
});
// Init GPT and disable initial ad load so that we can load targeting data first:
googletag.cmd.push(() => {
adSlot = googletag
.defineSlot(...)
.addService(googletag.pubads());
// Attempt to load Optable targeting key values from local cache, then load GAM ads:
optable.cmd.push(function () {
const tdata = optable.instance.targetingKeyValuesFromCache();
for (const [key, values] of Object.entries(tdata)) {
googletag.pubads().setTargeting(key, values);
}
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
});
</script>
<!-- Placeholder DIV for adSlot... referenced by googletag.defineSlot() above: -->
<div id="div-gpt-ad-12345-0"></div>
<script>
// Call Optable DCN for targeting data which will update the local cache on success.
optable.cmd.push(function () {
optable.instance.targeting().catch((err) => {
// Maybe log error
});
});
googletag.cmd.push(() => {
googletag.display(adSlot);
});
</script>Note that the above example fetches locally cached targeting key values and calls googletag.pubads().setTargeting() with them. Note also that the usual targeting() call is done as well, though its return value is ignored. This ensures that the local targeting cache is kept updated as activations are modified.
To automatically capture GPT SlotRenderEndedEvent and ImpressionViewableEvent and send log data to your DCN using the witness API, simply install GPT event listeners on the SDK instance as follows:
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<script>
window.optable = window.optable || { cmd: [] };
optable.cmd.push(function () {
// Assumes optable.instance was created as in the examples above.
optable.instance.installGPTEventListeners();
});
</script>Advanced usage: You can customize which GPT events are registered and which event properties to include, per event type, by passing an options object:
// Only listen to impressionViewable and emit only `slot_element_id`
optable.instance.installGPTEventListeners({ impressionViewable: ["slot_element_id"] });
// For slotRenderEnded, emit all properties. For impressionViewable, emit only the listed properties.
optable.instance.installGPTEventListeners({
slotRenderEnded: "all",
impressionViewable: ["slot_element_id", "is_empty"],
});The value for each event key can be "all" (to include all witness properties) or an array of property names from the set below (as mapped by the SDK):
advertiser_id, campaign_id, creative_id, is_empty, line_item_id, service_name, size, slot_element_id, source_agnostic_creative_id, source_agnostic_line_item_id.
If no argument is provided, both slotRenderEnded and impressionViewable are captured with all properties. Any other GPT event name can be used as a key too, and an empty array means all properties. Events are witnessed as gpt_events_<event_name> (for example gpt_events_slot_render_ended).
Note that you can call installGPTEventListeners() as many times as you like on an SDK instance, there will only be one set of registered event listeners per instance: calls after the first are ignored, including any options passed to them. Each SDK instance can register its own GPT event listeners.
A working example of both targeting and event witnessing is available in the demo pages.
The Optable Web SDK provides a method installGPTSecureSignals to pass user-defined signals to Google Ad Manager (GAM) Secure Signals. The method takes one or more { provider, id } objects as separate arguments, each representing a unique signal to pass to GAM, for example:
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/latest/sdk.js"></script>
<script>
const mySecureSignals = [];
mySecureSignals.push({
// Name of the provider
provider: "uidapi.com",
// ID to use in the Secure Signal
id: "uid2_token_goes_here",
});
window.optable = window.optable || { cmd: [] };
optable.cmd.push(function () {
// Assumes optable.instance was created as in the examples above.
optable.instance.installGPTSecureSignals(...mySecureSignals);
});
</script>Please refer to the list of approved Secure Signal providers. Please refer to the provider's integration documentation for the exact provider name and value to pass as a signal.
To build the signals from cached EIDs instead of by hand, secureSignalsFromEids(eids, filter?) builds one { provider, id } pair per EID source — GPT keys secure signals by provider, so the first EID that passes the filter and carries a non-empty uid wins its source — optionally filtered by sources, inserters or matchers (an empty or missing list means no constraint on that field).
The signals come from the last cached targeting response, so a first visit installs none until targeting() has resolved once.
With the npm package:
import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals";
const eids = sdk.targetingFromCache()?.ortb2?.user?.eids ?? [];
sdk.installGPTSecureSignals(...secureSignalsFromEids(eids, { sources: ["uidapi.com"] }));With the SDK loaded from a script tag, the helper is available under optable.utils:
<script async src="https://cdn.optable.co/web-sdk/latest/sdk.js"></script>
<script>
window.optable = window.optable || { cmd: [] };
optable.cmd.push(function () {
const eids = optable.instance.targetingFromCache()?.ortb2?.user?.eids ?? [];
optable.instance.installGPTSecureSignals(...optable.utils.secureSignalsFromEids(eids, { sources: ["uidapi.com"] }));
});
</script>You can verify the signal was correctly passed to GAM by searching for its value cached in localStorage under the key _GESPSK-<provider_name>.
The Optable Web SDK can integrate with Prebid.js to provide targeting data for real-time bidding. There are three main ways to integrate:
The HTML code snippet below shows how prebidORTB2FromCache() can be used to retrieve targeting data from the LocalStorage administered by the Optable SDK, and write Seller Defined Audiences (SDA) into prebid.js which is also loaded into the page, using pbjs.mergeConfig({ ortb2: ortb2 }) as documented in the prebid.js first party data documentation. The targeting() API is also called in order to retrieve and locally store the latest matching activations from dcn.customer.com/my-site.
Note that prebid.js bidder adapters can subsequently retrieve the data from the global config.
Deprecated:
prebidORTB2()andprebidORTB2FromCache()are kept for existing integrations. They builduser.datafromaudienceand put EIDs under the ORTB 2.5 pathuser.ext.eids. For new integrations, prefer theortb2field of the targeting response (EIDs underuser.eids) or the RTD module.
An example of how to install the SDA data through pbjs is shown below. The bidder in the example is only a placeholder: replace it and its params with your own SDA-compatible bidder adapter.
For a working demo showing a pbjs and GAM integrated together, see the demo pages section below.
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<!-- Prebid.js lib async load: -->
<script async src="prebid.js"></script>
<!-- Initialize Optable SDK, and targeting call early when possible: -->
<script>
window.optable = window.optable || { cmd: [] };
// Init Optable SDK via command:
optable.cmd.push(function () {
optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
});
// Call Optable DCN for targeting data which will update the local cache on success.
optable.cmd.push(function () {
optable.instance.targeting().catch((err) => {
// Maybe log error
});
});
</script>
<!-- Placeholder DIV for adSlot -->
<div id="div-gpt-ad-12345-0"></div>
<!-- Initialize prebid.js -->
<script>
window.pbjs = window.pbjs || { que: [] };
var PREBID_TIMEOUT = 3000;
var FAILSAFE_TIMEOUT = 5000;
var adUnits = [
{
code: "/22081946781/web-sdk-demo/box-ad",
mediaTypes: {
banner: {
sizes: [
[250, 250],
[300, 250],
[200, 200],
],
},
},
bids: [
{
bidder: "yourBidder", // Replace with your SDA-compatible bidder adapter
params: {
// Bidder-specific params
},
},
],
},
];
function initAdserver() {
if (pbjs.initAdserverSet) return;
pbjs.initAdserverSet = true;
// ... etc ...
}
pbjs.que.push(function () {
optable.cmd.push(function () {
const ortb2 = optable.instance.prebidORTB2FromCache();
pbjs.mergeConfig({ ortb2: ortb2 });
// ... etc ...
pbjs.requestBids({
bidsBackHandler: initAdserver,
timeout: PREBID_TIMEOUT,
});
});
});
setTimeout(function () {
initAdserver();
}, FAILSAFE_TIMEOUT);
</script>For bidder adapters that do not support SDA, but that do support targeting private marketplace deals to key values, you can use a similar approach to the Google Ad Manager integration with key values from local cache. For example, for the IX bidder adapter and IX bidder-specific FPD, you can encode the targeting key values as shown below:
<script>
// ...
// prior to pbjs.requestBids():
pbjs.que.push(function () {
optable.cmd.push(function () {
const tdata = optable.instance.targetingKeyValuesFromCache();
var fpd = {};
/*
* Flatten targeting key=values from Optable SDK targeting cache
* into a custom key value object, such that a key K with values
* V1, V2, ... in the Optable SDK targeting cache is transformed
* to look like:
* {
* K + V1: 1,
* K + V2: 1,
* ...
* }
*
* Note that + above indicates string concatenation.
*
* Optable DCNs have K configured to "optable" by default, so the
* above would result in a custom key value "optableaudienceKeyword=1"
* (use key + "_" + seg below if you want "optable_audienceKeyword=1")
* being set whenever the visitor is matched to the activated audience
* specified by audienceKeyword by the DCN.
*/
for (const [key, values] of Object.entries(tdata || {})) {
for (const seg of values) {
fpd[key + seg] = "1";
}
}
pbjs.mergeConfig({
ix: {
firstPartyData: fpd,
},
});
});
pbjs.requestBids(...);
});
</script>buildRTD() builds the config behind the Optable RTD provider, which merges cached EIDs into the auction's ortb2Fragments with per-source bidder routing and merge strategies. With waitForTargeting: true it waits — bounded by Prebid's auctionDelay — for the targeting response when the cache has no EIDs yet, and an isControlGroup callback gates serving for split tests:
import { buildRTD } from "@optable/web-sdk/lib/dist/core/prebid/rtd";
const rtd = buildRTD({ waitForTargeting: true, isControlGroup: () => isControlGroup });For the auction flow and the full option list, see the RTD module README.
If you send Email newsletters that contain links to your website, then you may want to automatically identify visitors that have clicked on any such links via their Email address.
To enable automatic identification of visitors originating from your Email newsletter, you first need to include an oeid parameter in the query string of all links to your website in your Email newsletter template. The value of the oeid parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using Braze to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the oeid parameter in the query string of any links to your website as follows:
oeid={{${email_address} | downcase | sha2}}The above example uses various personalization tags as documented in Braze's user guide to dynamically insert the required data into an oeid parameter, all of which should make up a part of the destination URL in your template.
tryIdentifyFromParams() is included in the script-tag bundle. With the NPM package, import the addon once so it is added to OptableSDK:
import "@optable/web-sdk/lib/dist/addons/try-identify";On your website destination page, you can call a helper method provided by the SDK which will attempt to parse and validate a given query string parameter as EID (defaults to oeid), when found, it will automatically trigger a call to Optable's identify API.
For example:
<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<script>
window.optable = window.optable || { cmd: [] };
optable.cmd.push(function () {
optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
// Identify using a valid EID (email SHA256) "oeid" query string parameter.
optable.instance.tryIdentifyFromParams();
// Or if the EID is being passed through a "email_sha" query string
// like https://www.mysite.com?origin=newsletter&email_sha=abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789&foo=bar
// optable.instance.tryIdentifyFromParams("email_sha");
// Optionally, you can provide a custom prefix as the second argument to tryIdentifyFromParams.
// This prefix will be used in the constructed identifier, allowing you to capture a value
// from the URL parameter even if it may not be a SHA256-hashed email.
// For example, optable.instance.tryIdentifyFromParams("email_md5", "c2");
// You can find a list of supported prefixes at https://docs.optable.co/optable-documentation/dmp/reference/identifier-types#type-prefixes
});
</script>identifyAndTokenize(sdk, id, options?) is the publisher-facing identity entry point: it normalizes a hashed email (bare ids get the e: prefix; already-prefixed and utiq: ids pass through), calls identify (in the control group too), and — outside the control group — tokenizes the id and merges the resulting EIDs into the rolling cache, announcing the write with the optable-targeting:change event. It skips repeat calls for the same id in a session, identify included (OPTABLE_TOKENIZE_DONE holds a fingerprint of the last id, never the id itself, so signing in as someone else in the same tab resolves them; switching back to the first id runs it again), re-runnable with the optableForceTokenize flag, and the guard resets on error so a failed tokenize can retry:
import { identifyAndTokenize } from "@optable/web-sdk/lib/dist/core/identify-tokenize";
window.optable.identifyAndTokenize = (id) => identifyAndTokenize(sdk, id, { isControlGroup: () => isControlGroup });options also controls the cache write: cacheKey (default OPTABLE_RESOLVED), and maxUidsPerEid and nonOverridingMatchers, both forwarded to mergeCache. Pass nonOverridingMatchers when tokenize runs alongside a targeting call that skipped the first-party matcher, so a first-party EID does not displace a third-party one already held on the same source:
identifyAndTokenize(sdk, id, { isControlGroup: () => isControlGroup, nonOverridingMatchers: ["mysite.com"] });Ids are URI-decoded first, and an id counts as already prefixed when it starts with 1–3 letters or digits and a colon. The returned { merged, staleUid2s } (null when skipped, in the control group, or when tokenize fails) lets the caller chain UID2 refreshes on the merged cache, for example with refreshStaleUid2s.
The Optable DCN issues a passport (a signed JWT) that is cached in browser localStorage. The passport encodes a unique visitor ID that the DCN uses to anonymously identify the browser. Both values can be read synchronously from the SDK:
const passport = sdk.passport(); // string | null — the raw JWT as stored in localStorage
const visitorId = sdk.visitorId(); // string | null — the `id` claim decoded from the passportBoth methods return null until the passport has been populated in localStorage. By default (initPassport: true) the SDK triggers a /config call at construction time, and the DCN response populates the passport.
If the returned value is null, the SDK logs a one-time warning per instance to help diagnose the cause. The two expected reasons for a null return are:
- The method was called before the passport was cached (e.g. before
sdk.site()resolved). - The DCN is configured to not echo the passport in response bodies, in which case the client-side cache is never populated.
The Optable Identity System is a cross-tenant identity system. On a DCN node configured to use it, the OIS ID replaces the visitor ID as the canonical profile identifier for collected events — the DCN makes that substitution itself, based on the node's identity selector.
An OIS-enabled node recognizes a browser two ways, and only one of them involves the SDK.
The DCN sets an OPTABLE_OID cookie and the browser attaches it to every call on its own, so identify() and profile() are already attributed to it with nothing enabled client-side.
That cookie is HttpOnly and scoped to optable.co, which has two consequences worth knowing. Its value is never readable from JavaScript — not via document.cookie, and not from the response, because Set-Cookie is a forbidden response header name. And because it is a third-party cookie for a publisher page, it is dropped wherever cross-site cookies are blocked (Safari/ITP, Firefox ETP, Chrome's third-party cookie restrictions) — a different problem from the first-party eTLD+1 case described under Domains and Cookies, and one a publisher cannot configure away. When that happens the DCN cannot recognize the browser from the cookie, and the derived identity below is what carries it instead. The SDK cannot bridge that gap: if the browser is willing to send the cookie it is already doing so, and if it is not, there is nothing to forward.
The DCN derives this identity server-side and returns it on the X-Optable-OID response header. With ois: true the SDK stores it and replays it on the same header, so the DCN recognizes the browser rather than deriving a fresh identity on every visit.
const sdk = new OptableSDK({
host: "dcn.customer.com",
site: "my-site",
ois: true,
});Or with a script tag:
<script type="text/javascript">
window.optable = window.optable || { cmd: [] };
optable.cmd.push(function () {
optable.instance = new optable.SDK({
host: "dcn.customer.com",
site: "my-site",
ois: true,
});
});
</script>
<script async src="https://cdn.optable.co/web-sdk/vX.Y.Z/sdk.js"></script>
⚠️ Requires DCN support. The node must have OIS ID derivation enabled and must exposeX-Optable-OIDto the browser. The DCN also only derives the identity for requests from a residential IP, so a VPN, datacenter or office IP returns no header. On a node without it the option is inert.
const id = sdk.oisId(); // string | null — the stored derived OIS ID
const state = sdk.oisState(); // { id, storageKey }
sdk.oisClear(); // forget it; the DCN returns a fresh derivation on the next callThe SDK dispatches an optable-ois:change event on window whenever the stored ID changes, so a page can react without polling. Its detail is { instance, id, storageKey }, where instance is the SDK's node, or its host when no node is set:
window.addEventListener("optable-ois:change", (e) => console.log(e.detail));oisId() returns null until a response has returned an ID. Unlike passport(), that does not happen during initialization: /config derives no identity, so the first ID arrives on the first identify(), profile() or targeting() call.
The ID is cached in localStorage under OPTABLE_OIS_<base64(host[/node])> as an opaque string, and sent back on X-Optable-OID.
Both directions are limited to the endpoints where the DCN derives an identity: /identify, /uid2/token, /profile and /v2/targeting. It is deliberately absent from /config — a custom header makes a request non-simple, and adding a CORS preflight to the SDK's initialization path would cost a round trip on every page load for an endpoint that returns no ID anyway — and from /witness, where the DCN records an event without deriving one.
There is no write policy to reason about. The DCN returns the identity it derived for the current request rather than the one the client replayed, so as the inputs it derives from drift (a new IP subnet, a browser upgrade) the stored value simply rolls forward. The SDK stores whatever the last response returned.
Nothing is stored and no header is sent without device access consent, so the option is a no-op when consent has not been granted.
Flags are per-session overrides for exercising SDK behaviour that is otherwise decided automatically — forcing a split-test variant, bypassing consent, turning on verbose logging. They are set from the page URL and read back through getFlags().
https://example.com/article?optableDebug&optableForceTargeting
A bare flag name means enabled, =0 means explicitly off. Flags supplied in the URL are persisted to sessionStorage, so a flag set once stays in effect for the rest of the tab session without re-appending the query string.
Use flagEnabled() for on/off flags, and getFlags() when a flag has more than two meanings:
import { flagEnabled, getFlags } from "@optable/web-sdk/lib/dist/core/flags";
if (flagEnabled("optableDebug")) {
console.log("[wrapper]", ...args);
}
// optableControlGroup is two-state: "1" forces control, "0" forces treatment.
const controlGroup = getFlags().optableControlGroup;Flag values are strings, and "0" is truthy in JavaScript, so do not test a raw value for truthiness — if (getFlags().optableDebug) is true for ?optableDebug=0. Use flagEnabled() instead.
A wrapper with flags of its own can persist them the same way with persistFlagsFromURL(["optableMyFlag"]); those are read back from sessionStorage directly and do not appear in getFlags().
These are a QA and debugging facility; none of them should be set on production traffic. For the full flag table and resolution order, see the flags README.
Resolves multiple Node Targeting Rules based on priority or aggregation.
This function is available under window.optable.utils as part of a collection of helper methods extending the SDK.
Define targeting rules:
const rules: NodeTargetingRule[] = [
{
targetingFn: async () => window.optable.node_sdk_instance_one.targeting(),
matcher: "your_domain",
mm: 3, // Authenticated
priority: 1, // Highest Priority (Optional)
},
{
targetingFn: async () => window.optable.node_sdk_instance_two.targeting("__ip__"),
matcher: "third_party_vendor",
mm: 5, // inference
priority: 2, // Lower Priority (Optional)
},
{
// Example with multiple identifiers
targetingFn: async () =>
window.optable.node_sdk_instance_three.targeting({
ids: ["identifier1", "identifier2"],
hids: [],
}),
matcher: "another_vendor",
mm: 5, // inference
},
];Call the resolver:
const result = await window.optable.utils.resolveMultiNodeTargeting(rules);
console.log(result);- If any rule has a
priority, the function returns theeids(and their sources and resolved IDs) of the highest priority that resolved any, 1 being the highest. Lower priorities (2, 3, etc.) are used only when every higher priority returned noeids. Rules with no priority, or a priority of 0 or below, never contributeeids. - If multiple nodes share the selected priority, merges their
eids. ortb2.user.data(audience segments) is always merged from every rule that resolved, whatever its priority.- If no priority is set, aggregates all responses.
type MultiNodeTargetingResponse = {
// All sources that resolved the response
eidSources: Set<string>;
// Identifiers that matched, across the selected rules
resolvedIds: Set<string>;
// IAB OpenRTB 2.6 Ortb2 User Object (Partial)
ortb2: { user: { eids: EID[]; data: Data[] } };
// Metadata referenced from the EIDs, renumbered so references stay unique across nodes
refs: Record<string, unknown>;
};type NodeTargetingRule = {
// Targeting function to resolve. e.g. window.optable.node_sdk_instance.targeting('__ip__')
// For multiple identifiers, use: window.optable.node_sdk_instance.targeting({ ids: ["id1", "id2"], hids: [] })
targetingFn: () => Promise<TargetingResponse>;
// (Optional) Technology provider domain; an EID's own matcher takes precedence
matcher?: string;
// (Optional) Match method (mm) based on IAB v26 standards; an EID's own mm takes precedence.
// Determines how the ID was matched. Possible values:
// 0 = unknown, 1 = no_match, 2 = cookie_sync, 3 = authenticated, 4 = observed, 5 = inference.
mm?: IDMatchMethod;
// (Optional) If provided we will only pick one resolved Ortb2Response from the most prioritize matcher.
// Any values below 1 will be threated as ignore
priority?: number;
};The geo-routing addon maps a visitor's region code to the Optable edge host that should serve them, so that a single SDK bundle can route traffic to the right regional edge.
import { getGeoRouting } from "@optable/web-sdk/lib/dist/addons/geo-routing";
const host = getGeoRouting(visitorRegion); // e.g. "us.edge.optable.co" for "US"
if (host) {
const sdk = new OptableSDK({
host,
node: "my-node",
site: "my-site",
});
}getGeoRouting returns null when the region is not supported, in which case region-specific initialization should be skipped. The default GeoMap supports the US (alias NA), CA, EU and AU region codes, each mapped to its regional edge host; pass a custom GeoMap as the second argument for other regions.
Keys are region codes, not country codes. Translating a visitor's country code to a region code (for example GB/UK → EU) is the caller's responsibility — the addon deliberately knows only regions. The caller also supplies the SDK node/site; this addon only resolves the host.
For the full region table and custom GeoMap usage, see the geo-routing addon README.
The bot detection addon identifies requests coming from known bots and crawlers, so a wrapper can skip work that only makes sense for real visitors — edge calls, identity resolution, analytics samples. It is a pure function over the user agent, with no network calls or storage access.
import { isBot } from "@optable/web-sdk/lib/dist/addons/botDetection";
if (isBot()) {
return; // Skip targeting and analytics for this request.
}With no argument it reads navigator.userAgent; pass a string to test one explicitly.
To skip the targeting call for bots, use SkipTargetingForBots(). It calls isBot() and, for a bot, sets the session marker that TargetingOncePerSession() checks (see below), so that helper skips the edge call too:
import { SkipTargetingForBots } from "@optable/web-sdk/lib/dist/edge/targeting";
if (!SkipTargetingForBots()) {
await sdk.targeting();
}To resolve targeting at most once per session, use TargetingOncePerSession(). The marker is sessionStorage["OPTABLE_TARGETING_DONE"], and the optableForceTargeting flag overrides it.
import { TargetingOncePerSession } from "@optable/web-sdk/lib/dist/edge/targeting";
const response = await TargetingOncePerSession(sdk);TargetingOncePerSession(sdk, input?) wraps the whole pattern: it skips the call when the marker is set, marks it on success, and resolves null rather than rejecting when the edge call fails, so identity resolution cannot break the page. A failed call leaves the marker unset, so a later page retries. A bot reads as already resolved, since SkipTargetingForBots sets the same marker.
The marker does not affect the Prebid RTD module: with waitForTargeting: true and an empty cache, a bot page still waits up to Prebid's auctionDelay. To avoid that, build the RTD config with buildRTD({ waitForTargeting: !isBot(), ... }).
Matching is substring-based and case-insensitive, covering generic crawlers, headless browsers, HTTP clients and Google's non-search agents. It is deliberately broad and user-agent only — a cost-saving filter, not a fraud signal. For the full match list, see the bot detection addon README.
The command queue addon lets a page interact with a wrapper loaded via an async script tag before the script has arrived, in the style of googletag.cmd and pbjs.que. The page queues functions on a plain-array stub; the wrapper replaces the stub with an instance, which drains the queue and executes later pushes immediately.
import { OptableCommands } from "@optable/web-sdk/lib/dist/addons/commands";
window.optable.cmd = new OptableCommands(window.optable.cmd || []);For the page-side stub and behaviour details, see the command queue addon README.
The EID cache merge module maintains a rolling EID cache across targeting and tokenize calls. New EIDs replace cached ones with the same source, sources absent from the new response are carried over, and UID2 EIDs past their refresh deadline are returned for the caller to refresh.
import { mergeCache, replaceCache } from "@optable/web-sdk/lib/dist/core/eid-cache";
const cached = JSON.parse(localStorage.getItem("OPTABLE_RESOLVED") || "null");
const { merged, staleUid2s } = mergeCache(replaceCache(await sdk.targeting()), cached);
localStorage.setItem("OPTABLE_RESOLVED", JSON.stringify(merged));mergeCache takes and returns cache format: refs keyed by EID source, no uids[].ext.optable.ref pointers. The wire format is unchanged everywhere else, so a response goes through replaceCache on the way in, while a cache read back from storage is already in that format.
For the merge rules and UID2 ref handling, see the EID cache README.
The UID2 refresh addon refreshes cached UID2 tokens directly against the UID2 operator, without a round trip to the Optable edge, and writes the outcome back to the targeting cache (a new token on success, the EID removed on opt-out or an invalid/expired token).
import { refreshStaleUid2s, refreshStaleUid2sFromCache } from "@optable/web-sdk/lib/dist/addons/uid2-refresh";
// After merging a response into the cache:
const { merged, staleUid2s } = mergeCache(replaceCache(response), cached);
await refreshStaleUid2s(sdk.dcn, staleUid2s);
// On a page that served targeting from cache and merged no response:
await refreshStaleUid2sFromCache(sdk.dcn);Neither call throws into the page; failures are logged under optableDebug. The addon also exports the lower-level refreshUid2Token and applyUid2Refresh. For outcomes and cache handling, see the UID2 refresh addon README.
The pubProvidedId module delivers cached EIDs to prebid through the pubProvidedId user-id submodule, for integrations that don't use the RTD module. EIDs from other providers are preserved, ours are replaced by source, and the work queues on the prebid global so it also runs before prebid has loaded. An isControlGroup callback gates delivery for split tests, the same way buildRTD takes one.
import { mergeIntoPubProvidedId } from "@optable/web-sdk/lib/dist/core/prebid/pubProvidedId";
mergeIntoPubProvidedId({ instances: ["pbjs"], isControlGroup: () => isControlGroup });On Prebid versions without the fix for prebid/Prebid.js#15562, the module's filtered ID refresh can drop other vendors (LiveIntent, ID5, …) from the page's first auction. Passing refreshAll: true works around it with a full ID refresh on the first merge: the upside is that no vendor is dropped from the first auction; the downside is that every ID vendor re-requests once on that pageview (relevant under per-request quotas) and the auction can start later. Leave it off on Prebid versions that include the fix.
For behavior details and options, see the pubProvidedId README.
The static mappings helper applies a wrapper's customer configuration defaults onto window.optable, so values a publisher sets on the page before the wrapper script loads always win. Plain-object config sections merge recursively; any other default only fills in a value the publisher left null or undefined.
import { setStaticMappings } from "@optable/web-sdk/lib/dist/core/staticMappings";
setStaticMappings({
defaultSite: "customer-sdk",
analytics: { tenant: "customer", sample: 0.1 },
});For the merge rules and a full wrapper example, see the static mappings README.
resolveId5(partnerId, options?) resolves an ID5 user id, loading the ID5 API on demand: QA flags first, then a local 7-day cache (its own key per partner, OPTABLE_ID5:<partnerId>), then a live resolution with ID5's own A/B holdout disabled. Live resolution is bounded (2s default, timeoutMs option) and resolves null on timeout, load failure, partner-id mismatch or the ID5 "0" placeholder. Pass the bot detection addon's isBot to skip live resolution for crawlers:
import { resolveId5 } from "@optable/web-sdk/lib/dist/core/id5";
import { isBot } from "@optable/web-sdk/lib/dist/addons/botDetection";
const id5Id = await resolveId5(id5PartnerId, {
isBot,
deviceAccess: () => sdk.dcn.consent.deviceAccess,
});The optableResolveID5ID and optableResolveId5 QA flags short-circuit resolution with a test value. Concurrent calls for the same partner share one script load and resolution, and an ID5 API already on the page is reused rather than loaded again.
deviceAccess gates the cache on both sides: when it returns false the id is neither read from nor written to localStorage, and every call resolves live. The gate is re-checked at write time, so consent withdrawn mid-resolution stops the write. On resolveId5 it defaults to allowed, the same posture as the SDK's own default consent, so pass sdk.dcn.consent.deviceAccess as above — omitting it on a page configured with consent.cmpapi would leave the ID5 cache ungated while every other SDK cache honours the CMP. ID5's own CMP integration separately gates its network call.
Reading the cache directly takes the same gate, as a required argument, so a cached user id cannot be read out of storage without stating a consent position:
import { getCachedId5UserId } from "@optable/web-sdk/lib/dist/core/id5";
const cached = getCachedId5UserId(id5PartnerId, () => sdk.dcn.consent.deviceAccess);The source check addon verifies that a page's site slug has a matching source configured in the DCN before the SDK is constructed, falling back to a default source when it does not — so multi-site publishers keep default enrichment on domains that were never provisioned.
import { checkSourceExists } from "@optable/web-sdk/lib/dist/addons/sourceCheck";
window.optable.site = await checkSourceExists({
site: window.optable.site,
defaultSite: window.optable.defaultSite,
node: "customer-node",
});The result is cached in sessionStorage, so the probe runs at most once per session for each site, node and host. A probe that times out (after 1500ms) falls back for that call only and is not cached, so a later page probes again. For the probe and fallback details, see the source check addon README.
setupAB assigns each visitor to a variant with traffic weighting, keeps the assignment sticky in localStorage (with a sessionStorage override for testing), and can stamp the variant onto Prebid.js bid requests for the Prebid analytics addon. Unlike the abTests config option, which picks a server-side test per targeting call, it is a client-side, per-visitor split.
import { setupAB } from "@optable/web-sdk/lib/dist/addons/abTestAssignment";
const ab = setupAB({
variants: [{ id: "production" }, { id: "test", trafficPercentage: 5 }],
});
if (ab.isControl) {
// Skip Optable for this visitor.
}For options and Prebid integration, see the A/B test assignment addon README.
getTrafficSource() classifies how the visitor arrived as one of direct, internal, paid, campaign, email, organic_search, social or referral, from the referrer and UTM / click-ID parameters, and returns { trafficSource, referrer? }. Only the referring hostname is reported, never the full URL. The first non-internal classification is cached in sessionStorage under optableTrafficSource and reused for the rest of the tab session. The Prebid analytics addon adds it to every payload.
import { getTrafficSource } from "@optable/web-sdk/lib/dist/core/traffic-source";
const { trafficSource, referrer } = getTrafficSource();setGAMSignalEnrichment({ control, enriched, context? }) sets a page-level GAM key-value, optableSignalEnrichment, for measuring Optable's impact in GAM reporting. Its value is <treatment|control>,<enriched|empty>,<context|nocontext>. It is set through googletag.setConfig, so it merges with the page's other targeting, and it fails silently on GPT versions without setConfig.
import { setGAMSignalEnrichment } from "@optable/web-sdk/lib/dist/core/gam-signal-enrichment";
setGAMSignalEnrichment({ control: isControlGroup, enriched: hasCachedEids, context: true });| Method | Description |
|---|---|
sdk.tokenize(id) |
Exchanges an identifier for EIDs via /v2/tokenize; resolves { user, refs? }. Used by identifyAndTokenize. |
sdk.uid2Token(id) |
Requests a UID2 token for an identifier via /uid2/token. |
sdk.resolve(id?) |
Resolves identity clusters via /v1/resolve; resolves { clusters, lmpid? }. |
sdk.site() / sdk.siteFromCache() |
Fetches (or reads from cache) the site configuration returned by /config. |
OptableSDK.sha256(value) |
Returns the SHA-256 hex digest of a string, with no normalization (unlike eid(), which lowercases and trims). |
OptableSDK.version |
The SDK version string. |
OptableSDK.TargetingKeyValues(tdata) |
Static form of targetingKeyValues(), for a targeting response you already have. |
OptableSDK.PrebidORTB2(tdata) |
Static form of the deprecated prebidORTB2(). |
The demo pages are working examples of both identify and targeting APIs, as well as an integration with the Google Ad Manager 360 ad server, enabling the targeting of ads served by GAM360 to audiences activated in the Optable DCN.
You can browse a recent (but not necessarily the latest) released version of the demo pages at https://demo.optable.co/. The source code to the demos can be found in the demos directory. By default the demo pages connect to the Optable demo DCN at ca.edge.optable.co (node optable) and reference the web site slug web-sdk-demo; override DCN_HOST, DCN_NODE and DCN_SITE when running make. The GAM360 targeting demo loads ads from a GAM360 account operated by Optable.
Note that the demo pages at https://demo.optable.co/ will by default rely on secure HTTP first-party cookies as described in this section. To see an example based on LocalStorage, see the index-nocookies variant here.
To build and run the demos locally, you will need Docker, docker-compose, make, mise (which installs the pinned toolchain, including pnpm), mkcert for the local TLS certificate, and envsubst:
cd path/to/optable-web-sdk
make
docker-compose upThen head to https://localhost:8180/ to see the demo pages. You can modify the code in each demo, then run make demos (and make build if you changed the SDK itself) and finally refresh the demo pages to see your changes take effect. If you want to test the demos with your own DCN, make sure to update the configuration (hostname and site slug) given to the OptableSDK (see webpack.config.js for the react example).
Note that using HTTP first-party cookies with a local instance of the demos pages pointing to an Optable DCN will not work because https://localhost:8180/ does not share the same top-level domain name .optable.co. We recommend using LocalStorage instead.
The Optable Identity System demo (/vanilla/ois.html, or /vanilla/nocookies/ois.html) covers both OIS identities: it explains why the OPTABLE_OID cookie identity is invisible to JavaScript, and shows the derived OIS ID the DCN returned, the localStorage key holding it, and the X-Optable-OID header sent and received on each call. It needs a DCN node with OIS ID derivation enabled, and only produces an ID for requests from a residential IP.
The npm demo shows the SDK installed as a package and bundled with webpack; see its README to build it.