Props Reference
Complete reference for all Brander component props
Required Props
apiKey
Your BranderUX project API key. Must start with bux_pk_. Generate it in your BranderUX dashboard at Projects → API Keys. At runtime the SDK exchanges it for a short-lived token, the raw key never rides the iframe URL.
<Brander apiKey="bux_pk_your_key" projectId="..." onQueryStream={...} />betaKey
Deprecated, use apiKey instead. Legacy bux_dp_ design-partner keys are still accepted during the migration window.
projectId
Your BranderUX project ID. Get this from your BranderUX dashboard. Each project has its own configuration, branding, and data sources.
<Brander apiKey="..." projectId="your_project_id" onQueryStream={...} />AI Handler (optional since 0.6.0)
onQueryStream
Streaming AI handler using AG-UI events. Provides progressive UI loading for the best user experience. Use with stream adapters: sseStream, anthropicStream, openaiStream, geminiStream.
Function Signature:
type StreamingCallback = ( params: CustomerAIParams ) => AsyncIterable<AGUIEvent> | ReadableStream<AGUIEvent>;
Usage with sseStream (Most Common):
import Brander, { sseStream } from "@brander/sdk";
<Brander
apiKey="bux_pk_your_key"
projectId="your_project_id"
onQueryStream={(params) => sseStream("/api/agent", { params })}
/>Usage with Provider Adapters:
import Brander, { anthropicStream } from "@brander/sdk";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({ apiKey: "..." });
<Brander
apiKey="bux_pk_your_key"
projectId="your_project_id"
onQueryStream={async function*(params) {
const stream = anthropic.messages.stream({
model: "claude-sonnet-5-5",
// Your persona + BranderUX UI instructions, append, never replace
system: YOUR_SYSTEM_PROMPT + "\n\n" + params.system,
messages: params.messages,
tools: params.tools?.anthropic,
});
yield* anthropicStream(stream);
}}
/>onQuery
Non-streaming AI handler. Returns complete response at once. Use for simple integrations that don't need streaming.
Function Signature:
type OnQueryCallback = ( params: CustomerAIParams ) => Promise<CustomerAIResponse>;
Usage:
import Brander from "@brander/sdk";
<Brander
apiKey="bux_pk_your_key"
projectId="your_project_id"
onQuery={async (params) => {
const response = await fetch("/api/ai", {
method: "POST",
body: JSON.stringify(params),
});
return response.json();
}}
/>CustomerAIParams (passed to your handler)
interface CustomerAIParams {
system?: string; // System instructions (A2UI protocol in flexible mode)
messages: Array<{
role: "user" | "assistant";
content: string;
}>;
tools?: MultiProviderTools; // Screen tools in all provider formats
max_tokens?: number; // Suggested max tokens
}
// Tools provided in all provider formats
interface MultiProviderTools {
anthropic: AnthropicTool[]; // Use with Anthropic Claude
openai: OpenAITool[]; // Use with OpenAI GPT
gemini: GeminiTool[]; // Use with Google Gemini
}Optional Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "hybrid" | "classic" | "chat" | "chat" | Display variant: hybrid (full playground), classic (site-focused), chat (inline messages) |
| defaultSidebarOpen | boolean | true | Default state of conversation sidebar in classic variant |
| language | string | "en" | BCP-47 tag for the widget's own copy and text direction ("he", "ar", "pt-BR"). RTL languages flip the layout automatically; English and Hebrew are built in, other languages are translated on first use |
| title | string | "Brander Widget" | Accessible name of the widget's iframe: what a screen reader announces when the visitor reaches the frame. Name it after the business, and give each widget its own name when a page carries more than one |
| conversations | Conversation[] | undefined | Initial conversations to load for persistence |
| activeConversationId | string | undefined | ID of currently active conversation |
| onConversationsChange | function | undefined | Callback when conversation state changes |
| actionHandlers | Record<string, ElementActionHandler> | undefined | Deterministic actions: keyed by the exact element action name (e.g. onAddToCart), or scoped to one element as "custom:<key>.onAddToCart". A registered action runs your handler in your app (your session and API clients) instead of becoming an AI query; return { followUpQuery } to run a query afterwards. Handler errors are logged, never retried. Find the names in the Element Library (Interactions → Copy actionHandlers) or via the MCP list_elements contract. |
| onConversion | (event: BranderConversionEvent) => void | undefined | Hosted mode: called in your page each time the agent collects a record (a lead, booking, order or request). The event is the type, the entity and the record id, never personal data. A brander:conversion DOM event carrying the same event fires on window either way. Errors are logged, never retried. |
| onConversationStart | () => void | undefined | Called in your page once per conversation, at the visitor's first action in it: the first designed page they open or their first question the agent answers. Not when the chat opens or the home loads by itself, and never twice for one conversation. Carries nothing. A brander:conversation-start DOM event fires on window either way. Errors are logged, never retried. |
| trackConversions | boolean | false | Forward each conversion to the tags already on your page: gtag (GA4 generate_lead), fbq (Meta Lead, or Schedule for a booking), a brander_lead event pushed to dataLayer and oaiq (ChatGPT Ads lead_created, or appointment_scheduled for a booking), each only when it exists. The start of each conversation goes too: Meta Contact and a brander_conversation_start event to gtag, dataLayer and oaiq. Loads no tag of its own, so your cookie banner keeps governing them. |
| visitSource | BranderVisitSource | false | Read from the page | Hosted mode: where the visitor came from (the utm_* tags, the gclid, fbclid and msclkid click ids, the referring site and the landing path), stamped on the records the agent collects. Read once per tab session by default; pass an object to supply your own, or false to send nothing. The prop is read once, when the component mounts, and later changes are ignored, so decide before mounting. The default capture is kept in this tab's session storage as brander:visit-source (not a cookie; list it wherever your site lists what it stores); false neither stores nor sends anything. |
| width | string | "100%" | Widget container width |
| height | string | "600px" | Widget container height |
| className | string | undefined | CSS class for container |
| style | CSSProperties | undefined | Inline styles for container |
Conversation Persistence
Use the conversation props to persist chat history across sessions:
import Brander, { sseStream } from "@brander/sdk";
import { useState, useEffect } from "react";
function App() {
const [conversations, setConversations] = useState([]);
const [activeId, setActiveId] = useState(null);
// Load from storage on mount
useEffect(() => {
const saved = localStorage.getItem("brander_conversations");
if (saved) {
const state = JSON.parse(saved);
setConversations(state.conversations);
setActiveId(state.activeConversationId);
}
}, []);
return (
<Brander
apiKey="bux_pk_your_key"
projectId="your_project_id"
onQueryStream={(params) => sseStream("/api/agent", { params })}
conversations={conversations}
activeConversationId={activeId}
onConversationsChange={(state) => {
setConversations(state.conversations);
setActiveId(state.activeConversationId);
localStorage.setItem("brander_conversations", JSON.stringify(state));
}}
/>
);
}Deterministic Actions (actionHandlers)
By default, every click in a generated screen becomes the next AI query. For actions that should call your API instead, add to cart, place order, subscribe, register a handler. It runs in your page (your session, your API clients), and the click never becomes a query. Unregistered actions keep the conversational behavior.
<Brander
apiKey="bux_pk_your_key"
projectId="your_project_id"
onQueryStream={(params) => sseStream("/api/agent", { params })}
actionHandlers={{
onPlaceOrder: async ({ item }) => {
await myApi.orders.create(item); // your backend, your session
return { followUpQuery: "Show my order confirmation" }; // optional
},
onAddToCart: async ({ item }) => {
await myApi.cart.add(item.id); // pure side-effect: no return
},
// Navigation actions (onSelectProduct, onView…) stay UNregistered so
// browsing remains conversational.
}}
/>Where the keys come from
Keys are the element's declared action names, verbatim, you never invent them:
- On the dashboard: Projects → Element Library → select an element → Copy actionHandlers, a ready-to-paste map with the exact keys (bodies as TODOs). Each element's publish review also lists its actions by name.
- Via the MCP:
list_elementsreturnsactions[]per element, name, meaning, item shape and a concrete example item, so your coding agent can generate the map in one call. - Typo safety: at runtime the SDK warns in your console when a registered key matches no element action.
Same action name on several elements
A bare key is a catch-all: registering onSelect fires that handler for every element declaring an onSelect, the payload's elementKey tells you which one fired. When two elements share a name with different meanings, scope the key to one element instead:
actionHandlers={{
// Only the product grid's onSelect, other elements' onSelect stays conversational
"custom:product-grid.onSelect": async ({ item }) => myApi.cart.add(item.id),
// Only the order list's onSelect
"custom:order-list.onSelect": async ({ item }) => openOrderPanel(item.id),
// Bare keys still work everywhere as before
onAddToCart: async ({ item }) => myApi.cart.add(item.id),
}}A scoped match wins over the bare name, and scoping is the safe way to handle one element's select/click without hijacking navigation on the rest. The exact scoped key ships as actions[].scopedKey in the MCP list_elements contract, and the dashboard's Copy actionHandlers emits scoped keys automatically whenever names collide.
Complete Example
import Brander, { sseStream } from "@brander/sdk";
function App() {
return (
<Brander
apiKey="bux_pk_your_key"
projectId="my-project-id"
onQueryStream={(params) => sseStream("/api/agent", { params })}
variant="classic"
width="100%"
height="700px"
/>
);
}