JavaScript API

The window.Benson API for single-page apps, headless storefronts, custom carts and consent. Generated from the TypeScript types the loader ships.

Reference
Updated 25 October 20265 min read
Jump to a section

The Benson script exposes window.Benson. Every method is safe to call before the script has loaded: use the queue form and Benson replays the calls once it's ready.

// Safe before the script loads: queued and replayed in order.
(window.Benson = window.Benson || []).push(["setCart", {
  currency: "GBP",
  totalMinor: 4200,
  items: [{ id: "merino-jacket-m", qty: 1, priceMinor: 4200 }],
  codes: [],
}]);

Methods#

This list is generated at build time from the BensonPublicApi type the loader ships, so it always matches the script.

version: string
The loaded script's version.
push(cmd: [string, ...unknown[]]): void
Queue form usable before load: (window.Benson ||= []).push(['setCart', {...}]).
ready(cb: () => void): void
Runs your callback once Benson has loaded (straight away if it already has).
setCart(cart: BensonCart | null): void
codes default to [].
page(path?: string): void
SPA navigation → page_view + module re-scan.
on(event: BensonEventName, cb: (e: unknown) => void): () => void
ext_detected also replays this page's and session's earlier detections to a late subscriber, so subscribing after detection still hears it.
state(): BensonState | null
The session's detections so far. Null before the runtime loads: call it inside ready.
getSessionId(): string | null
Null unless persistent ids are allowed; for headless order attribution.
debug(on?: boolean): void
Persists in sessionStorage.bn_debug.
registerApplyHandler(fn: BensonApplyHandler): voidoptional
Custom stores apply widget codes through this (queued until the widget loads).
widget?: BensonWidgetApioptional
Present once the widget module has loaded (calls before that are queued).

Cart#

Benson.setCart(cart) tells Benson what's in the basket on stores where it can't read the cart itself (headless, Hydrogen, custom checkouts). Shopify theme stores don't need it. Amounts are integers in minor units (pence, cents).

currency: string
totalMinor: number
items: BensonCartItem[]
codes: string[]
subtotalMinor?: numberoptional
Derived from items when absent.
itemCount?: numberoptional
Derived from items when absent.

Each item in items:

id: string
qty: number
priceMinor: number
Benson.setCart({
  currency: "GBP",
  totalMinor: 9600,
  items: [
    { id: "sku-123", qty: 2, priceMinor: 3600 },
    { id: "sku-456", qty: 1, priceMinor: 2400 },
  ],
  codes: ["AUTUMN15"],
});

// Empty or unknown cart:
Benson.setCart(null);

Single-page apps#

Call Benson.page() after each client-side navigation, so Benson records the page view and re-scans the new page:

router.afterEach((to) => Benson.page(to.fullPath));

Events#

Benson.on() subscribes to in-page events and returns an unsubscribe function. Events fire whether or not the session is sampled.

const off = Benson.on("ext_detected", (event) => {
  if (event.positive && event.actorClass === "extension") {
    console.log("Coupon extension:", event.actorId);
  }
});

You don't need to subscribe before Benson detects anything: a new ext_detected subscriber first hears what this page and this session have already detected, then each new detection. A detection from an earlier page carries its confidence band rather than the exact score.

The same events reach Google Tag Manager and GA4 through the dataLayer bridge as benson_ext_detected, benson_agent_detected, benson_ext_blocked, benson_widget_shown and benson_code_applied, once per session. See dataLayer and GA4 events for every field.

Stores that load third-party tools through Benson also get tool_on and state. See Tools.

Extension shoppers#

Benson.state() answers "does this shopper have a coupon extension?" without waiting for an event. It returns what the session has detected so far: extensions (ids such as "honey") and agents (AI agent and automation keys). It's null until the script has loaded, so call it inside Benson.ready():

Benson.ready(() => {
  const { extensions } = Benson.state();
  if (extensions.length) document.documentElement.classList.add("has-coupon-extension");
});

An empty list means "nothing seen yet", not "no extension": Benson keeps watching the page, so pair state() with Benson.on("ext_detected") for later detections. If you pass the result to another tool, apply your own consent rules first.

These are all the fields Benson.state() can return, generated from the BensonState type the loader ships. tools and x are covered under Tools.

extensions: string[]
Coupon and cashback extension ids, e.g. ["honey"].
agents: string[]
AI agent and automation keys, e.g. ["ai_agent:chatgpt_agent"].
agent?: BensonAgentState | nulloptional
The strongest positive agent/automation actor, or null.
tools?: string[]optional
The keys of the tools Benson loaded in this session. Present once the tools module runs (it augments state() itself; sites without tools keep the shape above).
x?: string | nulloptional
The session's encoded _benson_x (experiment and tool-holdout assignments), null unless getSessionId() is non-null. Present once the tools module runs.

Marking the page#

To style or script for extension shoppers without JavaScript of your own, turn on the page flag. Benson then sets data-benson-ext on the <html> element to the space-separated extension ids (for example honey karma), from the session's memory as soon as the script starts and again on each new detection:

html[data-benson-ext] .promo-code-field { display: none; }
html[data-benson-ext~="honey"] .honey-note { display: block; }

On Shopify, tick Mark extension shoppers on the page in the Benson app embed (theme editor → App embeds). With the snippet, add data-page-flag to the script tag. The Google Tag Manager template doesn't set it; on GTM installs, use Benson.state() instead. It's off by default because anything on the page, extensions and session-replay tools included, can read the attribute.

AI agents#

Benson recognises AI agents that browse your store for a person (ChatGPT agent, Perplexity Comet, Gemini in Chrome and others) and automation such as headless browsers. Three hooks let your team react, for example by skipping a newsletter pop-up, a chat launcher or an exit-intent offer that only gets in an agent's way.

The event. agent_detected fires each time Benson's confidence in an agent goes up. Act on it when positive is true:

Benson.on("agent_detected", (event) => {
  if (!event.positive) return;
  console.log(event.actorClass, event.actorId, event.confidence); // "ai_agent" "chatgpt_agent" 0.9
});

The state. Benson.state().agent is the strongest agent detected in the session so far, an AI agent before automation, or null for people:

Benson.ready(() => {
  const agent = Benson.state().agent; // { class: "ai_agent", id: "chatgpt_agent", confidence: 0.9 } | null
  if (agent?.class === "ai_agent") document.querySelector(".newsletter-popup")?.remove();
});

The page. Turn on Mark agent visits on the page under AI & agents → Settings → For developers in your dashboard. Benson then sets data-benson-actor (ai_agent or automation) and data-benson-actor-id on <html> on every page of the visit, so CSS alone can respond:

html[data-benson-actor="ai_agent"] .newsletter-popup { display: none; }
html[data-benson-actor] .chat-launcher { display: none; }

Detection needs the agent to run your pages' JavaScript, which agentic browsers do. Assistants that only fetch a page's HTML never run the script; on a custom store behind Cloudflare, the edge detector sees those too. The same detection reaches Google Tag Manager as benson_agent_detected.

A note for agents#

On plans with the agent policy, you can show detected AI agents a short note at the top of the page, such as where your live offers are listed. People never see it. Write it under AI & agents → Settings, in What agents read. It's plain text in the page, so agents that read the page's text or a screenshot both see it, and it links to your offer feed when that's on.

On Shopify, when the shopper's consent allows it, the cart and then the order also carry the result as the _benson_ext attribute (for example honey), which Liquid (cart.attributes._benson_ext), Shopify Flow and the Admin API can read.

Tools#

When your store loads third-party tools through Benson, the API also says which ones loaded in this visit:

  • Benson.state().tools lists the keys of the tools Benson loaded in this session, such as ["reviews_widget"]. A tool's key comes from its name when it's added, and never changes.
  • Benson.state().x is the session's experiment and measurement groups, encoded the way Benson writes them to the _benson_x cart attribute. Treat it as opaque. It's null unless Benson.getSessionId() returns an id.

Both fields appear once Benson's tools code runs, so they're missing on stores without tools. Read them with a fallback:

Benson.ready(() => {
  const tools = Benson.state()?.tools ?? [];
  if (tools.includes("reviews_widget")) console.log("The reviews widget loaded this visit");
});

Two events go with them:

// Once per tool per session, when all of the tool's scripts have loaded.
Benson.on("tool_on", (event) => console.log("Loaded", event.tool));

// When a tool loads or the session's groups change, with the new Benson.state().
Benson.on("state", (state) => console.log(state.tools, state.x));

A new tool_on subscriber first hears the tools already loaded in this session, one call each, then each new one. A tool whose scripts fail to load never fires tool_on. state doesn't fire for new extension detections: listen for ext_detected for those.

Every script Benson adds for a tool carries data-benson-tool with the tool's key, so document.querySelectorAll("script[data-benson-tool]") lists them.

On sites with manual consent, call Benson.consent() when the shopper decides. See Consent and storage.

myCmp.onChange((choices) => Benson.consent(choices.analytics === true));

Discount widget#

Once the widget has loaded, Benson.widget controls it for the current page view:

open(): void
close(): void
hide(): void
Hide the launcher and panel for this page view (merchant UI conflicts).

Custom checkouts can apply widget codes through their own function:

Benson.registerApplyHandler(async (code) => {
  const res = await fetch("/api/cart/discount", { method: "POST", body: JSON.stringify({ code }) });
  return res.ok ? { ok: true } : { ok: false, message: "That code can't be used on this basket." };
});

Headless order attribution#

Benson.getSessionId() returns the current session id when persistent ids are allowed (otherwise null). Add it to the order as a _benson_sid attribute so Benson can join the order to the session.

If you load tools through Benson on a headless Shopify store, add two more attributes next to it: _benson_tools, set to Benson.state().tools joined with commas, and _benson_x, set to Benson.state().x. Orders then show which tools were on, and count in a tool's measurement. Skip both when they're empty or null.

Debugging#

Add ?benson_debug=1 to any page URL, or call Benson.debug(true) and reload, to log what Benson is doing to the console: the config and modules it loaded, each public event (ext_detected, ext_blocked and the rest), every event it queues for your reports, and any error. The switch is kept in session storage until you close the tab.

A ?benson_test= link from your dashboard runs a test for one page view: a simulated coupon pop-up that Benson detects and hides, so you can see it work. Benson ignores the parameter unless it confirms the token belongs to your store, then removes it from the address bar. That page view stores nothing, bypasses sampling and strict consent, writes no cart attributes, and never reaches your reports. See Test Benson on your store.

Stuck? Email [email protected].

Find out what's leaking. Free forever, no card.

Free forever up to 25,000 sessions a month. Every Pro feature free for your first 14 days. No card. We'll email you about getting started. Unsubscribe any time.