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
| Attribute | Type | Description |
api-key required |
string |
Your publishable API key (sb_pub_ prefix). Never use a secret key here. |
Product Configuration
| Attribute | Type | Default | Description |
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
| Attribute | Type | Default | Description |
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
| Attribute | Type | Default | Description |
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
| Attribute | Type | Description |
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
| Attribute | Type | Default | Description |
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.
| Attribute | Type | Description |
step-1-name | string | Display name for step 1. |
step-2-name | string | Display name for step 2. |
step-3-name | string | Display name for step 3. |
step-4-name | string | Display name for step 4. |
step-5-name | string | Display name for step 5. |
step-6-name | string | Display name for step 6. |
step-7-name | string | Display name for step 7. |
step-8-name | string | Display 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.
| Attribute | Type | Fires on | Description |
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
| Attribute | Type | Default | Description |
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
| Attribute | Type | Default | Description |
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
| Attribute | Type | Default | Description |
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).
| Event | detail payload | When 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();
| Event | detail payload | When 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})`);
});