Widget

Widget Embedding Guide

The <salesbooth-deal> web component handles the complete deal flow and acts as the customer-facing surface for enabled Salesbooth agent capabilities. This guide covers all 44 attributes, theming, events, and advanced features.

Quick Embed

The minimum required setup — just your publishable API key and a product ID:

<script src="https://salesbooth.com/sdk/v1/salesbooth-widget.js"></script> <salesbooth-deal api-key="sb_pub_your_key" products="prod_a1b2c3d4" ></salesbooth-deal>
Publishable keys are safe for client-side use

Keys prefixed sb_pub_ have restricted widget scope: products:read, customers:write, deals:write, and agent:negotiate. They are limited to widget operations and cannot manage your account.

Agent Capabilities

The widget is the default delivery surface for customer-facing Salesbooth agents. Once the widget is installed, a tenant can enable approved sales, quote, negotiation, booking, promotion, support-to-sales, website, or offer-page capabilities without adding a separate script for each agent.

Agents use the API; customers use the widget

Agent decisions, tool calls, delegations, and approval checks happen through Salesbooth APIs. The widget presents the approved customer experience on the tenant website and keeps dangerous actions behind the same approval gates.

If a capability is not yet available through the widget, report the missing behavior as an agent support ticket or platform request instead of adding a private per-agent embed.

All Widget Attributes

The widget is configured entirely via HTML attributes. All attributes except api-key are optional.

Authentication

AttributeTypeDescription
api-key required string Your publishable API key (sb_pub_ prefix). Never use a secret key here.

Product Configuration

AttributeTypeDefaultDescription
products string Comma-separated product IDs. If omitted, customer selects from your full catalog.
product-selection string "single" Product selection mode. "single" or "multi".
saved-config-id string Pre-load a saved product configuration by ID. Skips the configuration step.
currency string "USD" ISO 4217 currency code. Customer sees prices in this currency.

Content & Copy

AttributeTypeDefaultDescription
title string "Complete Your Deal" Heading shown at the top of the widget.
cta-text string "Get Started" Text on the primary call-to-action button.
locale string "en" Language code for UI strings. Built-in: en, fr, de, es, ja, pt, zh.
locale-url string URL to a custom translations JSON file. Overrides the built-in locale strings.

Branding & Appearance

AttributeTypeDefaultDescription
theme-color string "#2563eb" Primary brand color (hex or CSS color). Used for buttons, progress, and accents.
dark-mode string "auto" Theme mode. Use "auto" to follow prefers-color-scheme, "true" to force dark, or "false" to leave dark mode unforced.
font-family string system-ui CSS font-family string. Applied to all widget text.
border-radius integer 12 (12px) Border radius in whole pixels for cards and buttons. Parsed with parseInt(...). E.g. "0", "4", or "16px".
logo-url string URL to your logo image. Shown in the widget header. Recommended: 32px height.
custom-css string URL to a CSS file injected into the widget's shadow DOM. For advanced customization.
button-style string "filled" Button variant: "filled", "outlined", or "text".

Customer Pre-fill

AttributeTypeDescription
customer-name string Pre-fill the customer name field. Useful for authenticated users.
customer-email string Pre-fill the customer email field.
customer-phone string Pre-fill the customer phone field.

Workflow & Steps

AttributeTypeDefaultDescription
steps string auto Legacy alias for step-order. Use a comma-separated list of step names such as "products,customer,payment,confirmation". Numeric values are not supported. Superseded by step-order when both are set.
step-order string Comma-separated list of step names defining a custom step sequence. E.g. "products,customer,payment,confirmation". Takes priority over the legacy steps attribute. The confirmation step is always appended if not included.
skip-steps string Comma-separated step names to skip entirely. E.g. "negotiate,contract". The payment and confirmation steps cannot be skipped.
data-step-conditions string (JSON) JSON object defining conditional step visibility based on deal state. Supported condition fields are minTotal, maxTotal, and dealType. E.g. '{"payment":{"minTotal":1}}' or '{"negotiate":{"dealType":"negotiable"}}'. Parsed once on widget init; invalid JSON is silently ignored.
contract-template string Contract template ID. If set, the contract step is shown with this template.
signature-mode string "checkbox" Signature capture mode: "checkbox", "type", "draw", or "digital".

Step Names

Override the display name for each step position. Names fall back to the widget config or the built-in locale strings if not set.

AttributeTypeDescription
step-1-namestringDisplay name for step 1.
step-2-namestringDisplay name for step 2.
step-3-namestringDisplay name for step 3.
step-4-namestringDisplay name for step 4.
step-5-namestringDisplay name for step 5.
step-6-namestringDisplay name for step 6.
step-7-namestringDisplay name for step 7.
step-8-namestringDisplay name for step 8.

JavaScript Callback Attributes

Lightweight alternative to addEventListener: set the attribute to a global function name and the widget calls it directly on the matching event.

AttributeTypeFires onDescription
on-complete string deal-created, payment-completed Global function name called when the deal is created or payment is completed. E.g. on-complete="myHandler" calls window.myHandler(detail).
on-error string error Global function name called when a widget error occurs. Receives { widgetId, code, message, step, recoverable }.
on-step-change string step-change Global function name called when the customer begins navigating to a new step. Receives { from, to, stepName }.

Analytics & Tracking

AttributeTypeDefaultDescription
analytics boolean true Enable session replay and funnel analytics. Set to "false" to disable.
analytics-consent boolean true Whether the user has given analytics consent. Set to "false" to suppress tracking.
analytics-callback string Global function name called with raw analytics events. E.g. "myAnalyticsHandler".

Performance

AttributeTypeDefaultDescription
cache-ttl number 300000 Cache TTL in milliseconds for product and config data. Default: 5 minutes.
mode string "online" Set to "offline" to enable offline-first mode (see Offline Mode).
offline boolean false Legacy boolean flag for offline-first mode. Set to "true" to enable. Prefer mode="offline" for new integrations.

Infrastructure & Internationalisation

AttributeTypeDefaultDescription
data-region string auto Optional manual region override. Accepted values: "au", "eu", "us". For normal embeds, omit this attribute and let the dashboard-provided widget config base URL or canonical Salesbooth origin route requests. Use direct regional routing only after the matching regional endpoint override and host infrastructure are live for that region; otherwise browser calls to dormant regional hosts can fail and make the widget appear offline.
data-fx-rate-stale-label string locale default Custom label displayed next to a price when the foreign-exchange rate used to convert it is stale. Overrides the built-in locale string. E.g. data-fx-rate-stale-label="Rate may be outdated".

Theme Customization

The widget supports three levels of customization: attributes, CSS variables, and a custom CSS file.

Attribute-based theming

<salesbooth-deal api-key="sb_pub_your_key" products="prod_a1b2c3d4" theme-color="#7c3aed" dark-mode="true" font-family="'Inter', sans-serif" border-radius="12px" logo-url="https://your-site.com/logo.png" button-style="outlined" ></salesbooth-deal>

Custom CSS (advanced)

For full control, link a custom CSS file. The widget uses shadow DOM, so your styles must be injected via custom-css:

custom-styles.css
/* Override widget CSS custom properties */ :host { --sb-primary: #7c3aed; --sb-primary-hover: #6d28d9; --sb-radius: 12px; --sb-font: 'Inter', sans-serif; } /* Override specific components */ .sb-btn-primary { text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; } .sb-header { border-bottom: 2px solid var(--sb-primary); }
<salesbooth-deal api-key="sb_pub_your_key" products="prod_a1b2c3d4" custom-css="https://your-site.com/custom-styles.css" ></salesbooth-deal>
CSS file must be on an accessible CORS-enabled URL

The custom-css URL is fetched by the browser. Ensure CORS headers allow requests from your page's origin, or host it on a CDN.

Dark mode

<!-- Always dark --> <salesbooth-deal dark-mode="true" ... /> <!-- Follow system preference (prefers-color-scheme) --> <salesbooth-deal dark-mode="auto" ... /> <!-- Toggle dynamically with JavaScript --> <script> const widget = document.querySelector('salesbooth-deal'); widget.setAttribute('dark-mode', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'true' : 'false'); </script>

Event Handling

The widget emits DOM CustomEvents. Listen via addEventListener or the sb.on() SDK method.

Widget DOM events

const widget = document.querySelector('salesbooth-deal'); widget.addEventListener('salesbooth:deal-created', (e) => { const { dealId, total, currency, customerEmail } = e.detail; // Redirect, track, provision access... }); widget.addEventListener('salesbooth:contract-signed', (e) => { console.log('Contract signed:', e.detail.contractId, e.detail.signatureMode); }); widget.addEventListener('salesbooth:step-change', (e) => { analytics.track('checkout_step_change', { from: e.detail.from, to: e.detail.to }); });

Custom element DOM events

These events are dispatched on the <salesbooth-deal> element. Listen with addEventListener('salesbooth:event-name', handler).

Eventdetail payloadWhen fired
Lifecycle
salesbooth:ready { widgetId, steps, productCount, hasCustomer, restored? } Widget initialized and config loaded
salesbooth:opened { widgetId, productIds, step } Widget opened programmatically or via trigger
salesbooth:widget-closed { widgetId } Popup widget closed by user or programmatically
salesbooth:abandoned { widgetId, step, timeSpent, productsSelected } User abandoned the flow (widget closed without completing)
salesbooth:error { widgetId, code, message, step, recoverable } Widget error (recoverable or fatal)
Step navigation
salesbooth:step-change { widgetId, from, to, stepName } Customer begins navigating to a different step
Products & cart
salesbooth:product-selected { widgetId, productId, name, price, quantity, selected } Product selection changed
Customer
salesbooth:customer-entered { widgetId, email } Customer email submitted in the customer step
Deals & contracts
salesbooth:deal-created { widgetId, dealId, total, currency, customerEmail, offline? } Deal submitted and confirmed by server
salesbooth:terms-accepted { widgetId, contractId, signatureId, signedAt } Contract terms accepted (backward-compatible alias for contract-signed)
salesbooth:contract-generated { widgetId, contractId, templateName, dealId } Contract generated from template
salesbooth:contract-signed { widgetId, contractId, signatureId, signedAt, verificationHash, signatureMode } Contract signature captured
Saved configurations
salesbooth:config-saved { widgetId, shortCode, shareUrl } Configuration saved for sharing
salesbooth:config-loaded { widgetId, shortCode, shareUrl, createdAt, expired, itemsRestored, skippedProducts, skippedOptions, priceChanges, invalidatedPromoCodes, adjustedQuantities } Saved config loaded and cart restored
salesbooth:template-loaded { widgetId, templateId, templateName, lineItemCount, discountCount, hasTerms, metadataKeys } Deal template data applied to the widget
Payment
salesbooth:payment-started { widgetId, dealId, amount, currency } Payment flow initiated
salesbooth:payment-completed { widgetId, dealId, paymentIntentId, amount } Payment successfully processed (visual widget)
Negotiation
salesbooth:negotiation-submitted { widgetId, dealId, proposedPrice, originalTotal, autoAccept? } Price negotiation request submitted
salesbooth:negotiation-accepted { widgetId, dealId, autoAccepted? } Negotiation accepted by seller (or auto-accepted)
salesbooth:negotiation-rejected { widgetId, dealId } Negotiation rejected by seller
salesbooth:negotiation-countered { widgetId, dealId } Counter-offer received from seller
salesbooth:negotiation-expired { widgetId, dealId } Negotiation timed out before a decision
Offline sync
salesbooth:offline { widgetId } Network connection lost
salesbooth:online { widgetId } Network connection restored
salesbooth:sync-started { widgetId, count } Offline deal sync begun
salesbooth:sync-completed { widgetId, synced, failed } Offline queue flushed on reconnect
salesbooth:sync-failed { widgetId, failed?, synced?, type?, description?, error?, status?, retryable? } Offline sync attempt failed (transient or permanent)
salesbooth:sync-abandoned { widgetId, deals, retries } Offline sync retries exhausted; deals abandoned
salesbooth:sync-validation-failed { offlineId, validation_errors } Offline deal items failed schema or stock validation on sync

Headless API events

Headless events are emitted by a SalesboothDeal.Headless instance in the script-tag build, or by the ESM HeadlessDeal export. Initialize with either a publishable apiKey or a public widgetId; the headless runtime will resolve the widget config before product loading. Subscribe with sb.on('event-name', handler). These are not DOM events and do not use the salesbooth: prefix.

const sb = new SalesboothDeal.Headless({ widgetId: 'YOUR_WIDGET_ID' }); await sb.init(); sb.on('step-changed', ({ from, to, availableSteps }) => { analytics.track('checkout_step_changed', { from, to, availableSteps }); }); sb.on('step-completed', ({ step, data }) => { console.log('Completed step:', step, data); });
import { HeadlessDeal } from 'https://salesbooth.com/sdk/v1/salesbooth-widget.mjs'; const sb = new HeadlessDeal({ widgetId: 'YOUR_WIDGET_ID' }); await sb.init();
Eventdetail payloadWhen fired
Step navigation
step-changed { from, to, availableSteps } Step navigation complete
step-completed { step, data } Customer completed a step and flow advanced
step-validated { step, valid } Step validation check completed
step-skipped { from, to } goToStep() jumped over one or more intermediate steps
steps-updated { steps } Active step list changed by setStepOrder(), skipStep(), or addStepCondition()
Products & cart
product-removed { productId, cart } Product removed from cart
cart-updated { cart, subtotal, itemCount } Cart contents changed
option-changed { productId, optionKey, value, priceImpact } A configuration option was changed
configuration-validated { productId, valid, errors } Per-product server-side validation completed
validation-complete { valid, errors, warnings } validateConfiguration() completed for all cart products
Pricing & discounts
totals-updated { subtotal, tax, discount, total, currency } Financial totals changed
price-updated { lineItems, subtotal, tax, discount, total, currency, … } Cart totals recalculated after any pricing change
discount-applied { code, type, value, amount } Discount applied to cart
discount-removed { code, newTotal, reason? } Discount removed from cart
Customer
customer-set { customer: { name?, email?, phone? } } Customer info set via setCustomer()
Deals
deal-completed { dealId, paymentIntentId, total, currency } Payment confirmed and deal fully closed
Payment
payment-initiated { paymentIntentId, amount, currency } Payment initiation started
payment-confirmed { dealId, paymentIntentId, amount, currency } Payment successfully confirmed
payment-previewed { subtotal, tax, total, … } previewPaymentAmount() returned breakdown (no payment intent created)
Negotiation
negotiation-update { dealId, status, rounds } Negotiation status or round data changed during polling
Subscriptions
subscription:created { dealId, billingCycle, data } Deal converted to recurring subscription
subscription:paused { dealId, data } Subscription billing paused
subscription:resumed { dealId, data } Paused subscription resumed
subscription:cancelled { dealId, options: { endOfPeriod?, reason? }, data } Subscription cancelled
subscription:usage_recorded { dealId, metricName, quantity, data } Metered usage recorded against a subscription

SDK event API (sb.on())

If you load the SDK separately from the widget, use the event emitter API:

// After loading salesbooth.js const sb = Salesbooth.init({ apiKey: 'sb_pub_your_key' }); // Subscribe to an event sb.on('deal.created', (data) => { console.log('Deal created:', data); }); // Subscribe to all events (wildcard) sb.on('*', (eventName, data) => { analytics.track(eventName, data); }); // Unsubscribe a specific handler const handler = (data) => { ... }; sb.on('deal.created', handler); sb.off('deal.created', handler); // Unsubscribe all handlers for an event sb.off('deal.created'); // Rate limit events sb.on('rate-limit-warning', ({ remaining, limit, reset }) => { console.warn(`API quota low: ${remaining}/${limit} remaining`); }); sb.on('rate-limited', ({ retryAfter }) => { showBanner(`Rate limited. Retrying in ${retryAfter}s...`); });

Offline Mode

The widget supports offline-first operation via a persistent IndexedDB queue. Deals created while offline are stored and automatically synced when connectivity is restored.

Enable offline mode

<salesbooth-deal api-key="sb_pub_your_key" products="prod_a1b2c3d4" mode="offline" ></salesbooth-deal>

How the offline queue works

// The widget automatically: // 1. Detects network loss via navigator.onLine + fetch failures // 2. Persists API requests to IndexedDB (database: "salesbooth_offline") // 3. Shows a queue badge: "2 deals pending sync" // 4. Replays the queue on reconnect using Idempotency-Key headers // 5. Removes successfully synced requests; keeps 5xx failures for retry // Listen for offline events const widget = document.querySelector('salesbooth-deal'); widget.addEventListener('salesbooth:offline', () => { showBanner('You are offline. Your deal will sync when connection is restored.'); }); widget.addEventListener('salesbooth:sync-completed', (e) => { showSuccess(`${e.detail.synced} deal(s) synced!`); });
Idempotency guarantees

All queued requests include an Idempotency-Key header. If a request is retried after a network failure, the server deduplicates it — you'll never create a duplicate deal.

Storage quota

// The widget monitors IndexedDB storage usage. // When usage exceeds 85% of quota, a warning is emitted: widget.addEventListener('salesbooth:error', (e) => { if (e.detail.code === 'STORAGE_QUOTA_WARNING') { console.warn('Offline storage near limit:', e.detail.message); } });

Config caching

The widget caches product and configuration data locally for the configured TTL. It also compares config change markers such as updated_at, version, and config_version to decide when cached product data should be refreshed.

// Control cache TTL via attribute <salesbooth-deal cache-ttl="600000" ... /> <!-- 10 minutes --> // Or disable caching entirely <salesbooth-deal cache-ttl="0" ... />

Mobile Responsiveness

The widget is fully responsive and touch-optimized by default. No additional configuration is required.

  • Fluid layout that adapts to any container width
  • Touch-friendly tap targets (minimum 44×44px)
  • Native mobile keyboard types on input fields
  • Swipe gestures for step navigation
  • Apple Pay / Google Pay on supported devices

Recommended container sizing

/* Let the widget fill its container */ salesbooth-deal { display: block; width: 100%; max-width: 480px; /* recommended max for single-column layout */ margin: 0 auto; } /* For modal/popup embed */ .checkout-modal salesbooth-deal { width: 100vw; max-width: 560px; height: 100%; overflow-y: auto; }
Test on real devices

Use your browser's DevTools device emulation for initial testing, but always verify on physical iOS and Android devices before launch — especially the payment step.

Config Caching & Performance

The widget uses local TTL-based caching with a background config refresh. Returning visitors can render immediately from cached data while the widget fetches the latest config in the background.

How config caching works
// First load: fetch and cache the config locally GET /api/v1/widget-config?widget_id=wgt_abc123 Authorization: Bearer sb_pub_xxx < 200 OK < { < "updated_at": "cfg_hash_v1", < "config_version": 2, < "products": "prod_1,prod_2" < } // Subsequent loads within the TTL: use cached config immediately, // then refresh in the background GET /api/v1/widget-config?widget_id=wgt_abc123 Authorization: Bearer sb_pub_xxx < 200 OK < { < "updated_at": "cfg_hash_v2", < "config_version": 3, < "products": "prod_1,prod_3" < } // If updated_at, version, or config_version changed, // the widget clears the related product cache and reloads it.

Widget config fetches always return a full JSON response. Cache invalidation is driven by the local TTL plus the config change markers returned in the payload.

Retry behavior

The SDK automatically retries failed requests with exponential backoff:

// Default retry schedule: // Attempt 1: immediately // Attempt 2: 1 second delay // Attempt 3: 2 second delay // Attempt 4: 4 second delay (max 3 retries by default) // Listen for retry events sb.on('retry', ({ attempt, retryAfter, status }) => { console.log(`Retrying... attempt ${attempt}, wait ${retryAfter}s (HTTP ${status})`); });

Next Steps