Documentation overview

Website SDK

One script tag connects your website to The AI CMO: visits, conversions, and revenue flow into your warehouse, journeys can react to what people do on your site, and website messages from Journeys render on your pages.

Install

In the app, open your account menu and choose Website SDK. Add your site by domain and you get a snippet to paste before </head>:

html
<!-- The AI CMO -->
<script async src="https://theaicmo.com/sdk/v1/tracker.js"></script>
<script>
  window.AICMO = window.AICMO || [];
  AICMO.push(['init', { siteId: 'lpo_your_site_id' }]);
</script>

That's the whole install. The script loads asynchronously and cannot block or break your page – even if analytics is unreachable, your site is unaffected.

To confirm it works, press Check again on the Website SDK page. It fetches your page and looks for the tag and your site key, and tells you what it found: installed, the tag carries a different site key (a snippet copied from another site), the page could not be reached, or not in the page HTML – which, if you install through a tag manager, only means the tag exists in the browser; the script then reports in by itself the first time a page loads. The page also re-checks quietly every few seconds while it waits. The signals, in order:

  • "Installed – the tag is on yoursite.com" or "Script loaded – seen on yoursite.com just now" – the tag is in the page, or the script ran on your site and fetched its settings. Either is the install confirmation.
  • "Receiving data – last seen …" – data is arriving. The first page view after the tag is live is enough; a conversion, a form submit or an identify call (the two extra lines of code in the next row) adds revenue and identity on top. The page banner then reads "SDK installed and receiving data".

If it still says not seen on the site yet, open a page that carries the script tag with a hard refresh and without an ad-blocker, then check again.

One site, several brands

A site belongs to the brand it was added on. When another brand of yours – a test brand, say – needs the same website, share the site instead of installing it again. On the Website SDK page of the brand that owns the site, open Other brands and choose the brand to share it with; on the other brand, the same page offers Use an existing site. The shared brand sees the same key, the same install state and the same settings, and can register events for it.

Two things stay put. Events land under the brand the key belongs to, and the snippet, settings and removal are managed on that brand. Brands that share a site share their event registrations: a key registered on either brand is accepted by the site and listed under Data → Events on both.

To rehearse an event-triggered journey on a test brand, add a site to the test brand and use its key on your staging environment. The tracking code stays identical; only the key differs.

What is tracked automatically

With no further code, the SDK captures pageviews (including single-page app navigation), visitors and sessions, campaign attribution from tracked links and UTM parameters, and how people behave on a page: how far they scroll (10, 25, 50, 75, 90 and 100 %), the buttons they see and click, the elements they see, exit intent, rage clicks, and forms they start and abandon. Everything else – your business moments, conversions, revenue, identity – you declare explicitly with the calls below, so your analytics contains what you decided matters, not noise.

On a site with a consent banner, all of this runs only as far as the visitor allowed – see Consent and privacy.

Session recording, heatmaps, and automatic click capture are switched off by design: they multiply event volume without adding decisions, and they are the first thing a privacy review flags.

Conversions, form submits, identify calls, your registered custom events and website-message interactions land in your warehouse with an sdk_ prefix on the event type, attributed to the visitor and – once they identify – to the person.

Behaviour (page views, scrolling, buttons and elements seen or clicked, exit intent, rage clicks, forms started and abandoned) is read from the analytics layer and copied into your warehouse about every ten minutes as web_ events – web_scroll, web_cta_click, web_form_abandoned and so on – so audiences and journeys can use them like any other event. Only what an identified visitor did crosses: identifying someone needs their marketing consent, so an analytics-only visitor's behaviour is never used for marketing, and an anonymous browser never becomes a contact. These rows count toward your warehouse volume like any other event. Each person also gets one daily site_activity row (sessions, pageviews, top pages).

Attribution: first click or last click

The SDK keeps a touch ledger per visitor: their first-ever arrival (source, medium, campaign, referrer, landing page, timestamp) is recorded once and never overwritten, and their most recent campaign or referral arrival updates alongside it. Direct return visits never erase a known source – the same "last non-direct click" convention the big analytics suites use. Both touches ride on every conversion and revenue event, stored in your own warehouse.

Which touch your reports credit is a per-brand setting: last click (the default) or first click. Because both touches are stored raw on every event, changing the model later never loses data – reports state the active model next to the numbers, and history keeps enough to be re-read under either. Attribution starts when the SDK is installed, and journeys that span devices connect once the person identifies on both.

Included analytics volume

Visitor analytics is included with your plan. How many events a month that covers depends on the plan you are on – your plan page carries the number. Conversions, identity, custom events, journeys, and revenue tracking are never counted against it: it meters only the behavioral layer (pageviews and sessions).

If a site outgrows its plan's volume, nothing breaks and nothing is lost silently: visitor analytics switches to sampling (1 in 10 visitors), you get an alert, and full detail returns when volume drops or the plan grows. First-party data – conversions, revenue, journey events – is never sampled.

When your plan ends, your sites stop collecting: the snippet stays on your pages but records nothing, and messages, personalization, Site chat and push go quiet. Resubscribe and they switch back on by themselves – no reinstall. A site you switched off yourself stays off.

The API

Call methods through the queue, which is safe to use before the script has loaded:

js
AICMO.push(['method', ...arguments]);
MethodWhat it does
init({ siteId, consent?, nonce? })Starts the SDK. Pass consent: 'pending' to wait for a consent banner, or the choice itself when your server knows it – see Consent and privacy. nonce is your page's Content Security Policy nonce, when the script tag itself does not carry it.
track(eventName, properties)Sends a custom business event – see below.
identify(id, { email?, hash? })Tells the SDK who the visitor is – your own customer ID, an email, or both. hash is your server's signature – see Signed identities. Sign every call: Site chat shows a customer named without one to your team as not verified.
trackConversion(type, { value, currency, email })Records a conversion with revenue.
trackPageView()Manual pageview, for cases the automatic tracking can't see.
consent({ analytics, marketing, support? })The visitor's choice, passed on every page load from your consent banner's record. null when there is no choice on record. support is optional – see Consent and privacy.
optIn() / optOut()The original on/off switch – every category, remembered on this device.
consentStatus()Returns granted (analytics and marketing), partial (one of them), denied (neither) or pending (no choice yet). Support is not part of it.
chat(command, …)Opens and controls Site chat from your own code – see Site chat.
reset()Call it on your site's logout: the browser forgets who the person was – the stored identity, its signature, the last value read from your identity cookie and their chat. When someone had been identified, it also gives the browser a fresh visitor ID, starts a new anonymous person in the visitor analytics and clears their inbox messages from the page.

Custom events

Custom events are how your site's own business moments – a deposit, a booking, a quote request – reach the platform:

js
AICMO.push(['track', 'deposit', { value: 250, currency: 'USD' }]);

Or with zero JavaScript, declaratively on any element:

html
<button data-aicmo-event="deposit_click" data-aicmo-value="50">Deposit</button>

Two rules to know:

  • Event keys must be registered first in Data → Events, and use lowercase letters, numbers, and underscores. Events with unregistered keys are not stored – but the Events tab shows keys it has seen so you can register them with one click.
  • In the warehouse the event lands as sdk_<key> (a registered deposit becomes sdk_deposit), and a journey with the matching custom-event trigger enrolls the person the next time the engine polls – within about a minute.

You can also define no-code capture rules in Data → Events: a CSS selector or URL match that fires an event without touching your site's code, reading the value from the page or a cookie.

You can also ask the chat: describe the moments you want to measure and it proposes the keys, registers them here, and hands you the snippet for each – see Chat → Your website's events.

Identifying people

Anonymous visitors get a random visitor ID. The moment you know who they are, tell the SDK – with the identifier you key customers on, signed by your server (see Signed identities):

js
// your own customer ID
AICMO.push(['identify', 'player-48213', { hash: SIGNATURE }]);
// or their email
AICMO.push(['identify', 'jane@acme.com', { hash: SIGNATURE }]);
// both, when you have both
AICMO.push(['identify', 'player-48213', { email: 'jane@acme.com', hash: SIGNATURE }]);

Sign every identify() call. Your warehouse and journeys take an unsigned one too, but Site chat needs the signature to know who is chatting, and personal content – person-targeted website messages, the inbox, personalization – can be set to require it. The identity cookie below is never signed, so it does not replace a signed identify() for Site chat.

Pick the one the rest of your data carries. If your CSVs, database pull or API calls key people on your own ID, identify with that ID and the website joins the same record; if email is what you have everywhere, use email. Pass both when both are known and they resolve to one person – the warehouse keeps every identifier it has seen for someone alongside the others. Anything with an @ is read as an email, anything else as your ID.

Identification is what connects website behavior to a real customer: their events join their profile, journeys can enroll them, and person-targeted website messages can reach them. If your site already sets a login cookie, configure it as the identity cookie in Data → Events and the SDK identifies people automatically – no code change. Identities read from the identity cookie are not signed, so Site chat shows those customers to your team as not verified (as anonymous with Require signed identities on), and personal content set to require signed identities does not reach them; to have their chats verified, also call identify() with { hash } from your page.

Signed identities

Your site key is in your page's HTML, so anyone can claim to be anyone. Where something is meant for one person – a person-targeted website message, their inbox, personalization by audience, their Site chat conversation – the identity can be signed: your server computes an HMAC-SHA256, as hex, of the person's e-mail or your own customer ID, in lower case, with the site's identity secret, and the page passes it along:

js
AICMO.push(['identify', 'jane@acme.com', { hash: SIGNATURE }]);

E-mails and customer IDs are compared in lower case, so a customer ID must belong to one person whatever its letter case – your customer number, never a username the customer chose.

The secret is under Identity verification on this page – Reveal or Copy, one per site – and never belongs in the browser. With Require signed identities on, an identity without a valid signature gets only what every visitor gets; anonymous visitors are unaffected. Site chat always needs the signature before it attaches a conversation to a customer's record. A customer named without a valid signature shows to your team as not verified – never with their record, their History or their chat on other devices – and, with Require signed identities on, is ignored. A customer who logs in during a chat they wrote in within the last hour is attached to it within seconds, on the chat's next check. A second, chat-only signature lets a signed-in customer's chat follow them to another device and lists their earlier conversations under History – see Site chat → Their chat on every device. The Site chat studio's Identity tab has the same secret and switch, and says how the site's logged-in customers arrive – signed, unsigned, or with a signature that does not match.

Rotate gives a site a new secret. The current one keeps working for 7 days, so your server can switch first; the card says until when. After that, anyone your server still signs with the old secret counts as not signed in. Only if the secret leaked, Rotate and stop it now stops the old one at once. Only an administrator of the brand the site belongs to can see and rotate its secret or switch Require signed identities – its creator, an administrator of your organisation, or a teammate who can edit the brand, never one who can only view it – and each reveal, rotation and change of the requirement is listed in the admin portal's Audit Logs.

From version 1.29 of the script, the requests that fetch a person's website messages, inbox and personalization carry the e-mail, customer ID, signature and visitor ID in the body of the request, never in its address – so they do not end up in access logs, proxies or the browser's history. Every site loads the newest script from the same address, so there is nothing to change on yours.

Revenue

Send revenue either through a conversion or on a custom event, with whatever else you know at that moment:

js
AICMO.push(['trackConversion', 'signup', {
  value: 49, currency: 'EUR',
  external_id: 'player-48213',     // or email: 'jane@acme.com'
  channel: 'affiliate',            // your own fields ride along
  affiliate: 'aff-771',
  plan: 'pro', country: 'CZ',
}]);
AICMO.push(['track', 'deposit', { value: 250, currency: 'USD', method: 'card' }]);

Once you have called identify, every conversion and event carries that identity by itself – the email or external_id on a conversion is only for the moment you learn it there, such as a signup form.

Your own fields. Anything beyond value, currency and the identifier is kept on the event in your warehouse as its properties – the marketing channel a registration came through, an affiliate or promo code, the plan, the market. Up to 20 fields per event; text, numbers and true/false (text is clipped at 200 characters, nested objects are dropped). Where the visitor came from – tagged links, tracked links, ad click IDs – is recorded on every event by itself; pass your own channel field for what the visitor told you or your system knows.

Those fields then do work. An audience can be "everyone who signed up where channel is affiliate" (Customers → Audiences → From event → Where the event has); a journey's On event trigger, and a Wait for event step, can open only when a field has a value; the customer's timeline shows each field on the event; and a message in that journey can use it as {{p.channel}}.

The value becomes the event's amount in your warehouse, and feeds journey goals, revenue reporting, and What's Working.

Website messages

When a journey publishes a website message, the SDK renders it on your site – as a banner, card, or modal – styled to the message, isolated from your page's CSS, and shown at most once per day per message. Person-targeted messages appear only to the identified person they were written for. Impressions, clicks, and dismissals are tracked automatically and feed the journey's performance numbers.

The inbox

Messages can persist in an on-site inbox instead of – or alongside – the one-shot popup. When a visitor has inbox messages, the SDK shows a small launcher in the corner with an unread badge; opening it lists their messages, newest first, and marks them read. Visitors with no messages never see the launcher at all. With Site chat on for the visitor, the messages sit in the chat panel's Inbox tab instead, and one launcher carries both.

Three things put messages there: the journey Website step (choose "Popup + inbox" or "Inbox only" on the step), promos – when a promo with a customer message is credited to someone, that message lands in their inbox automatically, with its terms attached – and the Inbox page (sidebar), where you manage the whole channel.

Managing the inbox

On the Inbox page every message shows opens, clicks and click-through rate, counted per person rather than per page load, next to who it targets, where it came from, and its state – Live, Scheduled, Draft or Ended. You can compose a new message, save it as a draft, schedule it to appear from a date and time, pin it to the top of the panel, edit or duplicate it, end it early, or delete it. A message leaves the inbox on its own when the period you chose runs out.

Who a message goes to: every visitor, one of your saved audiences, one person by email, or one person by your own customer or player id – useful when people sign in with an account number rather than an email address. Person-targeted messages are checked against your suppression lists before they send, and audience and site-wide messages are checked again for each reader, so someone who has opted out or self-excluded sees no marketing. Messages marked as Service – an account, payment or security notice – still reach them, and are shown first in their inbox. Anyone on a blacklist set to "Blocks everything" sees nothing at all.

What a message can contain

Beyond a headline, message and image:

  • Up to three buttons, each with its own link and treatment – solid, outline, or a plain link. A button with no link simply acknowledges the message and settles the row, which is what you want for "Got it". Each button is tracked separately, so you can see whether people clicked "Claim bonus" or "See terms".
  • A code to copy – shown as a tap-to-copy chip, for a bonus or discount code.
  • A label above the headline – a short word like BONUS in your accent colour.
  • The deadline, shown as "Ends in 2 days" and read from when the message actually expires, so the card and the truth can never disagree.
  • Terms, as text or a link.

How a message looks

The Design section sets the defaults for the whole site – what a card looks like (layout, image shape, button treatment) and what the panel looks like (accent, position, launcher, font, text size, corners, language, title). A sample card beside the controls shows the defaults as you change them.

Each message can then override any of it in its own Design tab: layout, image shape, its own accent, background and text colours, and an accent edge for something important. Anything you leave alone follows the site-wide defaults, so a message only looks different where you meant it to. Where a card carries two buttons, the second one steps down a level so two solid buttons never compete. The preview beside the form is rendered by the same code your visitors run, so what you see is what they get.

Messages can also be translated. The panel itself already speaks ten languages; the Translations tab translates the message inside it – label, headline, body and every button label – and a reader whose language you have not translated sees your original copy.

The Inbox page also documents the two public endpoints for rendering the same inbox natively inside your own logged-in UI instead of the SDK widget.

Site chat

The same script can put a chat on your site: visitors write to your team from any page, and you answer in Helpdesk. Switch it Off, Test or Live in the Site chat row on the Website SDK page, and design it under Design. With Site chat on, your inbox messages move into the chat panel's Inbox tab – one launcher for both. Everything about it – the test link, the studio, phones, replies to visitors who left, the photos and PDFs visitors may attach, signed-in customers, their chat on every device and their History of earlier conversations, consent and the code to open it – is in Site chat.

Personalization

The SDK can change what your site says per visitor. Mark an element with data-aicmo-slot="hero-title" (or give a CSS selector on the rule), then create rules under Audience → Audiences → Personalization: what to show, and who sees it – everyone, or one of your saved audiences. Higher-priority rules win; identified visitors on your exclusion lists are never personalized. Variants are plain text by default; HTML variants exist behind a per-site switch you have to turn on deliberately.

Rules can target devices – all, mobile (phones and tablets), or desktop – so the shorter headline shows only where screens are small.

Rules apply on page load and on navigation in single-page apps. Applied elements carry a data-aicmo-personalized attribute so your team can see what changed and why.

Each rule reports how often it applied and, for identified visitors, the revenue those people generated after first seeing it – the rule's row under Personalization reads both, so a rule is never a guess.

A/B testing a rule

When creating a rule you can switch on A/B test this change: write a second version, choose the split (say 50/50), and set a holdout – a slice of visitors who keep seeing your original page but still count as exposed. Every visitor lands in the same arm on every visit, and the rule's row then reads revenue per arm – A vs B vs holdout – with the leader highlighted. The holdout answers the question that matters: does the change beat doing nothing?

Customers who are inside a journey with a control group are left out of rule A/B tests automatically – they see your original content and are not counted. One person is never part of two experiments at once, so journey uplift numbers and rule test numbers both stay honest.

Web push notifications

Turn browsers into a push channel – no third-party push provider needed. Browser subscriptions are off by default: nothing is offered or collected from visitors until you switch them on for the site (Website SDK page → Web push notifications). Once on, two steps:

  1. Host a one-line service worker at your site root, saved as /aicmo-sw.js (browsers require this file to live on your own domain):

    js
    importScripts('https://theaicmo.com/sdk/v1/push-sw.js');
    
  2. Call AICMO.registerPush() from your own button – for example an "enable notifications" toggle in your account area. Browsers punish permission prompts the visitor didn't ask for, so the SDK never prompts on its own.

That's it. Subscribed browsers become reachable by the Push step in Journeys. Subscriptions are matched to the person once you identify them – someone who subscribes anonymously and logs in later becomes addressable automatically. AICMO.unregisterPush() removes this browser's subscription.

This is mobile push without an app: on Android phones, notifications arrive like any app's – even with the browser closed. On iPhone they arrive once the visitor has added your site to their home screen (Apple requires your site to ship a web app manifest for that). Desktop Chrome, Edge, and Firefox work everywhere. Blacklists and self-exclusion are enforced before every send.

Mobile apps (React Native)

Your React Native app gets the same three things through @aicmo/react-native – no dependencies, and no third-party push provider:

  • Events and identity – identify, track, and trackConversion land in your warehouse exactly like the website SDK's.
  • Push, delivered directly – upload your Firebase service account on the Website SDK page (verified live, stored encrypted) and The AI CMO sends notifications straight through your own Firebase project: Android natively, iOS via Firebase's APNs integration. Your app passes its FCM token with registerPushToken, and the Push step in Journeys reaches the device – matched to the person after identify.

The package README walks through setup for bare React Native and Expo. Blacklists and self-exclusion are enforced before every send, on this route like every other.

Survey pages

Your hosted survey pages can carry your site's SDK too: in a survey's Fill page & branding → Tracking section, pick the site and survey visits land in your analytics, with a survey_completed event fired on submit. GA4 and Meta Pixel IDs are supported there as well – IDs only, never scripts. Owner previews are never tracked.

If your site asks visitors for cookie consent, the SDK follows the answer: it runs only what the visitor allowed, runs nothing before they choose, and deletes what it stored when a choice is withdrawn or expires.

Three categories

CategoryWhat it turns on
AnalyticsPage views, visitors and sessions, scroll depth, clicks on calls to action, form timing (never what is typed), page performance – the visitor charts. Anonymous: the person is never named here.
MarketingIdentifying people, campaign attribution (first and last touch, UTM parameters, ad click IDs such as gclid and fbclid), conversions and custom events in your warehouse, website messages, the inbox, personalization, web push.
SupportSite chat and nothing else: the visitor's messages, an e-mail address they give for replies, and the page path, device type, language and time zone of the conversation. No tracking, no identify() values, nothing for journeys or your warehouse – a signed chat identity (AICMO.chat('identify', …)) can still tell the chat who a logged-in customer is. An unsigned one is not used: a claim needs marketing. Optional – not stated, it follows marketing.

With analytics and marketing allowed, everything on this page works as described, Site chat included. With neither, the SDK tracks nothing, identifies nobody and loads nothing more; it keeps only the choice itself, and a withdrawal by someone it already knew is noted in their consent history (see below). Site chat then runs only if your site allows support.

With analytics alone, identify does nothing, and an email or customer ID you put on an event is left out. With marketing alone, conversions, identity, messages and attribution work, and the visitor is not counted in page views or sessions.

Support is for sites that want to offer the chat to visitors who have not allowed marketing – a player who declined marketing but needs help, for instance. Pass it with the others, or on its own:

js
AICMO.push(['consent', { analytics: false, marketing: false, support: true }]);
AICMO.push(['consent', { support: true }]);

Support merges with the other two. A consent call that leaves support out keeps what the page said about it earlier; a call that carries only support leaves analytics and marketing exactly as they were – a choice still pending stays pending. Not stated at all, support follows marketing – or, on a site without consent mode, the chat works for everyone who has not turned the SDK off with optOut(). consent(null) clears support along with the rest, so a page that passes null and still offers the chat passes { support: true } after it. Which of your banner's categories gives support is your decision.

Passing the choice

Start the SDK in consent mode and pass the visitor's choice with consent on every page load:

js
AICMO.push(['init', { siteId: 'lpo_your_site_id', consent: 'pending' }]);
AICMO.push(['consent', { analytics: true, marketing: false }]);

Every page load, because your banner's record is the source of truth: the SDK never runs a page on a choice it remembers from an earlier one. Also call it when the visitor changes their choice in your banner, so the change applies without a reload. Pass null when there is no choice on record – a first visit, or a choice that expired. The values are true or false; anything else counts as not allowed.

Until the choice arrives the SDK waits, and the calls your own code makes – track, identify, trackConversion – wait with it. Once it arrives they run as far as the choice allows; after a refusal they are dropped. You do not need to wrap them in consent checks yourself.

Your banner keeps the choice in a cookie

Read the cookie and pass it. Here the banner stores three digits – necessary, analytics, marketing – so 110 means analytics without marketing:

html
<!-- The AI CMO -->
<script async src="https://theaicmo.com/sdk/v1/tracker.js"></script>
<script>
  window.AICMO = window.AICMO || [];
  AICMO.push(['init', { siteId: 'lpo_your_site_id', consent: 'pending' }]);
  function aicmoConsent() {
    var m = document.cookie.match(/(?:^|;\s*)site_consent=([01]{3})/);
    var v = m ? m[1] : null;
    AICMO.push(['consent', v ? { analytics: v[1] === '1', marketing: v[2] === '1' } : null]);
  }
  aicmoConsent();
</script>

Then call aicmoConsent() once more in your banner's save handler, after it has written the cookie. Change the cookie name and the digit positions to match your banner.

The cookie is HttpOnly

A page cannot read an HttpOnly cookie, but your server can. Have the server write the choice into init on every page it renders – null when there is no cookie:

html
<script>
  window.AICMO = window.AICMO || [];
  AICMO.push(['init', { siteId: 'lpo_your_site_id', consent: { analytics: true, marketing: false } }]);
</script>

When the visitor changes their choice on the page, your banner's code knows the new choice – pass it with AICMO.push(['consent', { analytics: …, marketing: … }]). From the next page on, the server renders it.

In a single-page app the choice holds for the whole visit; you pass it again only when it changes.

When the choice expires or is withdrawn

  • The SDK keeps nothing longer than your banner does. When your consent cookie expires or is deleted, your page passes null (or the server renders it), and on that page load the SDK stops and deletes everything it stored on the device. A choice that no longer allows a category deletes that category's entries the same way.
  • Withdrawing on the page takes effect at once. The withdrawn category stops sending immediately – page views, clicks and leaving the page included – and its entries are deleted. Turning analytics back on after withdrawing it takes effect from the next page load.
  • Every change is recorded. A new or changed choice appears in the person's consent history as web tracking – "granted" when it allows both categories, "granted (analytics only)" or "granted (marketing only)" when it allows one, "withdrawn" when it allows none. You see it on the person's Compliance and Activity tabs (open the person from Customers) and in the Consent lookup under Audiences. Passing the same choice on every page does not add rows, and an expired choice is not recorded as a withdrawal. Support is kept with the record but never decides it: granted or withdrawn follows analytics and marketing alone, and a call that carries only support adds no row.

The original on/off switch

optIn() and optOut() still work, for banners with a single yes or no. optIn() allows every category – analytics, marketing and support – and is remembered on this device for later visits. optOut() refuses all three, deletes what the SDK stored, Site chat's entries included, and is remembered, so the SDK stays silent on later visits. If your banner has categories, or its choice expires, use consent instead.

What the SDK keeps in the browser

The SDK sets no cookies. What it keeps lives in your site's localStorage (and, for the entries marked so, sessionStorage, which ends with the browser tab). <site> is your site key.

NameCategoryWhat it holds
aicmo_consent_<site>NecessaryThe last choice passed – analytics, marketing, and support when a page stated it – or the optIn/optOut decision; what keeps the SDK silent after a refusal
aicmo_visitor_<site>Analytics or marketingA random 12-character visitor ID; replaced by a fresh one when AICMO.reset() signs out someone who had been identified
ph_aicmo_ph… (also in sessionStorage)AnalyticsThe visitor analytics layer (PostHog): a pseudonymous ID, the session, cached settings
__ph_opt_in_out_<key>AnalyticsLeft by a withdrawal on a page where analytics was running; removed on the next page load
aicmo_ph_off_<site>AnalyticsSet only when visitor analytics is off for this site (the site is paused, or this visitor is outside its sampling share), so the next page waits for the setting instead of starting analytics; removed once analytics is on again
aicmo_touch_<site>MarketingFirst and last campaign arrival: source, medium, campaign, referrer, landing page, time
aicmo_link_attr_<site>MarketingThe last tracked link or UTM campaign
aicmo_click_ids_<site>MarketingAd click IDs (gclid, gbraid, wbraid, fbclid, ttclid, msclkid) with their time; sent with events for 90 days
aicmo_identity_<site>, aicmo_extid_<site>, aicmo_idsig_<site>MarketingThe email, your customer ID and the identity signature passed to identify; also deleted by AICMO.reset()
aicmo_autoid_<site>MarketingThe last value read from your identity cookie, so a repeat page load does not identify again; also deleted by AICMO.reset()
aicmo_msg_<message>MarketingWhether a website message was seen or dismissed, for its once-a-day limit
aicmo_pz_exp_<rule>MarketingA personalization rule already reported as seen by this person
aicmo_support_conv_<site>MarketingLeft by SDK versions before 1.28 (the old support tab); no longer written, and deleted with the marketing entries
aicmo_push_<site>MarketingThis browser subscribed to web push
aicmo_arrival_<site> (sessionStorage)MarketingThis tab arrived from a campaign link
aicmo_chat_<site>SupportSite chat: a random key for this browser's conversation, the conversation, a short fingerprint of a signed-in identity, when the newest message arrived, whether it was resolved, the unread count. Also deleted 30 days after the last message, and by AICMO.reset() or AICMO.chat('reset')
aicmo_chat_ui_<site> (sessionStorage)SupportSite chat in this tab: the panel open or closed, an unsent draft, messages not yet confirmed as sent (with the names of the files attached to them), previews dismissed, the reply address given here, the unread count. Also deleted by AICMO.reset() or AICMO.chat('reset')
aicmo_chat_test_<site> (sessionStorage)SupportThis tab opened Site chat's test link

Browser storage has no expiry date of its own. Each entry is kept while its category is allowed and deleted on the first page load where it no longer is – so with the choice passed on every page, nothing outlives your banner's record. Site chat's entries are deleted only when support stops being allowed – never with the marketing entries.

The only cookies the SDK reads are the ones you configure in Data → Events for identity or value capture, and it reads them only with marketing allowed.

Do Not Track

Do Not Track is respected by default for the analytics category: a browser sending DNT is not counted in page views or sessions. A visitor who accepts analytics on your own consent banner (consent mode, or optIn()) is counted anyway – their choice on your site wins over the browser-wide signal. Without a banner, DNT stands; respectDNT: false in init stops the SDK from reading it at all. It never stops the marketing category – consent does.