Switchfly Widgets Integration Guide (1.0.0)
Download OpenAPI specification:
Embeddable travel widgets for partner sites.
Search Widget: Full-featured travel search (air, hotel, car, activities, bundles)

Hotel Deals Widget: Showcase featured hotel deals with click-through to booking
Identity Widget: Headless utility — resolves a stable
profileId(UUID or provided ID) and sets it in shared config for all other widgetsUser Carts Widget: Displays a user's in-progress and saved carts as a horizontally scrollable card row with deep-link CTAs
The Switchfly Search Widget is a comprehensive travel search solution that adapts to your brand and customer needs. It provides a consistent, accessible user experience across all product types while offering extensive customization options for visual styling, language support, and product availability.
Supported Languages
The widget includes built-in translations for English, French, Spanish, Japanese, Simplified Chinese, and Thai. All UI text, labels, error messages, and validation prompts are automatically localized based on the locale parameter.

Custom Product Tabs
Product tabs are fully configurable—control which travel products appear (bundles, air, hotel, car, activities) and in what order. You can also inject custom external tabs (e.g., "Cruises" or "Deals") at specific positions within the tab row.

Branding & Theming
Colors, accents, and interactive states can be customized using branding tokens. The widget supports hex, RGB, HSL, and CSS variable formats, allowing seamless integration with your existing design system.

Search Errors & Validation
The widget includes comprehensive client-side validation and displays server-side errors returned from the booking platform. Error messages are localized and presented in accessible modal dialogs with clear messaging and recovery options.

Datepicker & UX Patterns
All date selection uses a consistent, compact datepicker interface with range highlighting, constraint enforcement (minimum/maximum dates, product-specific rules), and keyboard navigation support. Time selection is enabled for product types that require it (e.g., car rentals).

Recent Searches
The widget tracks and surfaces the user's recent searches by default. To disable, explicitly omit recent from a custom availableProducts list — no history will be stored and the tab will not appear.
- A Recent tab appears in the product selection bar whenever the user has qualifying search history. The tab is hidden when no history exists.
- Up to 5 recent searches are stored, newest first.
- Each recent search is stored in
localStorageunder the keyswf_recent_searches, scoped to the CDN origin serving the widget iframe. Storage is shared across all pages that embed the widget from the same origin. - Clicking a recent search card re-fires the original search immediately (no form prefill step).
- Entries are automatically aged out when:
- The travel start date has passed (or falls within the product's minimum advance-booking window).
- The entry is older than 90 days.
⚠️ IMPORTANT: targetUrl is Required
All Switchfly widgets require an explicit targetUrl parameter. Widgets will not function and will display a configuration error if targetUrl is not provided.
targetUrlspecifies your booking platform domain (e.g.,https://your-booking-site.com)- Widgets use
targetUrlas the base URL for API calls and search result navigation - Widgets do not default to any Switchfly environment - this prevents accidental requests to internal/test systems
- Both script embed and iframe embed modes require
targetUrl
SSO-Gated Loyalty Clients: For booking platforms protected by SSO (Single Sign-On), widgets work seamlessly *when configured properly to do so within Switchfly). When a user submits a search, the platform preserves the originally requested search URL through the SSO authentication flow and automatically redirects the user back to their search results after successful login. NOTE This is currently only supported for Switchfly's Standard Loyalty SSO.
- **Minimum supported width: 300px (below that, layout may stack / truncate)
- **Recommended embed width: 360px+
Widget Host vs Target URL
When integrating Switchfly widgets, it's important to distinguish between two URLs:
- Widget Host (
your-cdn.com): The domain where widget HTML pages and JavaScript assets (IIFE bundle) are served from. This is used in:- Script embed:
<script src="https://your-cdn.com/dist/switchfly-widgets-search.iife.js">(or the catchallswitchfly-widgets.iife.js) - Iframe embed:
<iframe src="https://your-cdn.com/widgets/search/...">
- Script embed:
Bundle options:
switchfly-widgets-search.iife.js— search widget only (recommended for clients embedding only the search widget)switchfly-widgets.iife.js— all widgets (use when embedding multiple widget types on the same page)Target URL (
targetUrl): The booking/search platform domain where the widget redirects users when they submit a search. This is configured as:- Script embed:
targetUrl: 'https://your-booking-site.com'(JavaScript config property) - Iframe embed:
targetUrl=https%3A%2F%2Fyour-booking-site.com(URL parameter, URL-encoded)
- Script embed:
In most deployments, the widget host and target URL are the same domain (e.g., both https://your-booking-site.com). However, they can be different if widgets are hosted on a separate CDN.
Shared Configuration (configure())
When embedding multiple widgets on the same page, use SwitchflyWidgets.configure() to set
shared options once. All subsequent create* calls inherit these values; per-widget options
always take precedence over the global config.
Shared fields (recognised by all widgets):
targetUrl, cobrand, locale, availableProducts, defaultProductType, profileId
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
// Set once — inherited by every widget on the page
SwitchflyWidgets.configure({
targetUrl: 'https://your-booking-site.com',
cobrand: 'partner123',
locale: 'en',
availableProducts: ['bundle', 'hotel', 'car'],
defaultProductType: 'bundle',
});
// Per-widget calls only need what's unique to each widget
SwitchflyWidgets.createSearchWidget({ containerId: 'search-widget' });
SwitchflyWidgets.createChatWidget({ containerId: 'chat-widget', chatApiUrl: 'https://your-chat-api.com' });
</script>
configure() can be called multiple times — each call merges into the existing global config.
Options passed directly to create* always win over global config.
Direct Script Embed (Search Widget)
Load the widget bundle and call createSearchWidget():
<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets-search.iife.js"></script>
<script>
SwitchflyWidgets.createSearchWidget({
containerId: 'search-widget',
targetUrl: 'https://your-booking-site.com',
locale: 'en',
cobrand: 'partner123',
searchParam: '|city:7321',
prefillDestinationLabel: 'San Diego',
searchDateFormat: 'dd/mm/yy',
customTab1Label: 'Cruises',
customTab1Url: 'https://example.com/cruises',
customTab1Position: 'last',
branding: {
tokens: {
accentColor: '#0a66c2'
}
}
});
</script>
Iframe Embed (Search Widget)
<iframe
src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com&locale=en&searchParam=%7Ccity:7321&prefillDestinationLabel=San%20Diego&cobrand=partner123&searchDateFormat=dd/mm/yy&customTab1Label=Cruises&customTab1Url=https://example.com/cruises&customTab1Position=last&accentColor=%230a66c2"
width="100%"
height="600"
frameborder="0">
</iframe>
Direct Script Embed (Hotel Deals Widget)
<div id="hotel-deals"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
SwitchflyWidgets.createHotelDealsWidget({
containerId: 'hotel-deals',
targetUrl: 'https://your-booking-site.com',
dealsApiUrl: 'https://api.example.com/v1',
searchParam: 'LAS',
locale: 'en',
maxItems: 6,
title: 'Featured Hotels',
branding: {
tokens: {
accentColor: '#dc2626'
}
}
});
</script>
Iframe Embed (Hotel Deals Widget)
<iframe
src="https://your-cdn.com/widgets/hotelDeals/?targetUrl=https%3A%2F%2Fyour-booking-site.com&locale=en&maxItems=6&accentColor=%23dc2626"
width="100%"
height="600"
frameborder="0">
</iframe>
Identity Widget (Script Embed only)
A headless utility widget — no UI. Resolves a stable profileId for the current user and
sets it in shared config so all subsequent widgets inherit it automatically.
Resolution flow:
- If
profileIdis explicitly provided (e.g. from SSO), it is used as-is andsourceis'provided'. - Otherwise, a UUID is generated via
crypto.randomUUID()and persisted inlocalStorageunder the keyswf_pid_{cobrand}. Subsequent page loads reuse the same UUID;sourceis'anonymous'. - Calls
SwitchflyWidgets.configure({ profileId })internally, so widgets created aftercreateIdentityWidget()automatically receive the resolved ID via shared config.
Resolution is synchronous. The onProfileResolved callback fires immediately (same tick).
It is safe to call createUserCartsWidget() inside the callback or immediately after.
Pattern A — Anonymous UUID (default)
<script src="https://your-cdn.com/dist/switchfly-widgets-identity.iife.js"></script>
<script>
SwitchflyWidgets.createIdentityWidget({
cobrand: 'partner123',
onProfileResolved(profileId, source) {
// source === 'anonymous'; profileId is the UUID from localStorage
SwitchflyWidgets.createUserCartsWidget({ /* ... */ });
}
});
</script>
Pattern B — SSO / authenticated profile ID
When your host page has an authenticated session, pass the user's profile ID explicitly. The Identity Widget skips UUID generation and propagates the provided ID to all widgets.
<script src="https://your-cdn.com/dist/switchfly-widgets-identity.iife.js"></script>
<script>
SwitchflyWidgets.createIdentityWidget({
cobrand: 'partner123',
profileId: '<%= current_user.traveler_profile_id %>', // server-rendered
onProfileResolved(profileId, source) {
// source === 'provided'
SwitchflyWidgets.createUserCartsWidget({ /* ... */ });
}
});
</script>
| Option | Type | Required | Description |
|---|---|---|---|
cobrand |
string | ✅ | Client identifier. Used to scope the localStorage key (swf_pid_{cobrand}). |
profileId |
string | — | Explicit traveler profile ID (e.g. loyalty number). Skips UUID generation. |
onProfileResolved |
function | — | Callback invoked with (profileId, source) once the ID is resolved. source is 'provided' or 'anonymous'. |
Notes:
- The UUID persists across browser sessions for the same
cobrandas long as the user has not cleared browser storage. - Anonymous mode is not supported in private/incognito browsing —
localStorageis cleared when the private session ends. - If your booking platform uses SSO, anonymous UUIDs will not match authenticated sessions on the booking side. Use the
profileIdoption after authentication to correlate accurately. - Calling
createIdentityWidget()multiple times with the samecobrandis safe — each call updates shared config but reuses the existing UUID fromlocalStorage.
User Carts Widget (Script Embed only)
Fetches a user's in-progress and saved carts from the user-carts service and renders them as a horizontally scrollable row of cards. Each card deep-links back into the booking platform with the original search parameters pre-filled.
Requires a resolved profileId — use the Identity Widget to obtain one automatically, or pass it directly.
Supported product types:
| Product | Card shows | Deep link |
|---|---|---|
| Hotel | Property name, city/state, check-in → check-out dates, guest count | Hotel search results pre-filtered to the property (switchflyId) |
| Car rental | Company name, pickup location, pickup → return dates/times | Car search results pre-filtered to the car type (car_id = carCode:companyCode) |
| Activity | Activity name, location | Activity search results with activity pre-selected (activity_id) |
Cart status display rules:
IN_PROGRESS— shown with a "Recently Viewed" badgeSAVED— shown with a "Saved" badgeCLOSED,BOOKED— hidden (not rendered)- If no
IN_PROGRESSorSAVEDcarts exist, an empty-state message replaces the card row
Card dismissal: The × button on each card removes it from the DOM client-side. Dismissal is visual only — it does not call the API. The card reappears on the next page load.
API call:
The widget makes a single GET request on mount:
GET {userCartsApiUrl}/carts?userId={profileId}&clientName={clientName}
An x-api-key header is added if apiKey is configured. If profileId is missing at mount
time, the widget renders a configuration error instead of making the API call.
Quick start (with Identity Widget):
<!-- 1. Resolve identity first -->
<script src="https://your-cdn.com/dist/switchfly-widgets-identity.iife.js"></script>
<script>
SwitchflyWidgets.createIdentityWidget({
cobrand: 'partner123',
onProfileResolved() {
// 2. Create carts widget once profileId is in shared config
SwitchflyWidgets.createUserCartsWidget({
containerId: 'user-carts',
clientName: 'partner123',
targetUrl: 'https://your-booking-site.com',
userCartsApiUrl: 'https://api.example.com/v1/user-carts',
title: 'Pick up where you left off',
});
}
});
</script>
<div id="user-carts"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets-carts.iife.js"></script>
Standalone (profileId already known):
<div id="user-carts"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets-carts.iife.js"></script>
<script>
SwitchflyWidgets.createUserCartsWidget({
containerId: 'user-carts',
profileId: 'user@example.com',
clientName: 'partner123',
targetUrl: 'https://your-booking-site.com',
userCartsApiUrl: 'https://api.example.com/v1/user-carts',
apiKey: 'your-api-key',
});
</script>
Fixed-height card row (branding):
Set branding.tokens.trackHeight to pin the card row to a specific pixel height. This also
controls the card image height, which expands to fill the available space. Omit to size by content.
SwitchflyWidgets.createUserCartsWidget({
// ...
branding: { tokens: { trackHeight: 220, accentColor: '#dc2626' } }
});
| Option | Type | Required | Description |
|---|---|---|---|
clientName |
string | ✅ | Client identifier passed to the carts API (clientName query param). |
targetUrl |
string | ✅ | Booking platform base URL used to construct deep-link URLs on cards. |
userCartsApiUrl |
string | ✅ | Base URL for the user-carts service (e.g. https://api.example.com/v1/user-carts). |
profileId |
string | ✅* | User profile ID. Automatically inherited from Identity Widget via shared config. |
apiKey |
string | — | API key sent as x-api-key request header if the service gateway requires it. |
containerId |
string | — | DOM element ID for the widget container. Defaults to user-carts-widget. |
title |
string | — | Heading displayed above the card row. Defaults to 'Pick up where you left off'. |
searchDateFormat |
string | — | Date format used in deep-link URL params (date1/date2). One of: mm/dd/yy (default), dd/mm/yy, yy/mm/dd, yyyy/mm/dd. |
utmSource |
string | — | UTM source appended to all deep-link URLs. |
utmMedium |
string | — | UTM medium appended to all deep-link URLs. |
utmCampaign |
string | — | UTM campaign appended to all deep-link URLs. |
utmContent |
string | — | UTM content appended to all deep-link URLs. |
branding.tokens.fontFamily |
string | — | CSS font-family value (e.g. "Noto Sans, sans-serif"). Inherits widget default if blank. |
branding.tokens.trackHeight |
number | — | Fixed height in pixels for the card row (e.g. 220). Controls card height. Omit to size by content. |
branding.tokens.bgColorWidget |
string | — | Widget background color. Defaults to #ffffff. |
branding.tokens.accentColor |
string | — | Primary accent color (status badge background, CTA links). Defaults to #3060f9. |
branding.tokens.accentColorHover |
string | — | Accent hover state color. Defaults to #0634C6. |
*profileId is required but is typically set automatically by the Identity Widget.
postMessage API
Widgets communicate with parent windows via postMessage for various events:
Widget → Parent Messages:
SWF_WIDGET_HEIGHT- Height changed (for iframe auto-resize)swf.widgets.search.navigate- Navigation request (required for sandboxed iframes)
Complete Parent Page Snippet (Iframe Embed):
<!-- Error handler: include once per page. Handles ERROR_MESSAGE redirects from Core. -->
<script src="https://your-cdn.com/dist/switchfly-widgets-error-handler.iife.js"></script>
<!-- Optional: set locale explicitly (auto-detected from <html lang> or ?locale= otherwise) -->
<script>SwitchflyWidgetsErrorHandler.init({ locale: 'en' });</script>
<script>
var iframe = document.getElementById('search-widget-iframe');
// Listen for widget messages
window.addEventListener('message', function(event) {
// Validate origin matches iframe src
try {
var iframeOrigin = new URL(iframe.src).origin;
if (event.origin !== iframeOrigin) return;
} catch (e) {
return;
}
var data = event.data;
// Auto-resize iframe
if (data && data.type === 'SWF_WIDGET_HEIGHT') {
var height = parseInt(data.height, 10);
if (height > 0) {
iframe.style.height = height + 'px';
}
}
// Handle navigation requests (required for sandboxed iframes)
if (data && data.type === 'swf.widgets.search.navigate') {
var targetUrl = data.payload && data.payload.url;
var openInNewTab = data.payload && data.payload.openInNewTab;
if (targetUrl) {
if (openInNewTab) {
window.open(targetUrl, '_blank');
} else {
window.location.assign(targetUrl);
}
}
}
});
</script>
Sandboxed Iframe Navigation (HubSpot, etc.)
Problem: Some CMS platforms (like HubSpot) render iframes in sandboxed contexts without the allow-top-navigation permission. When the widget tries to navigate via window.top.location.href, the browser throws a security error.
Solution: The widget automatically detects iframe mode and sends navigation requests via postMessage instead of attempting direct navigation. The parent page listener performs the actual navigation.
How It Works:
- Widget detects it's running in an iframe (
window.self !== window.top) - On search submit, widget sends
swf.widgets.search.navigatemessage to parent - Parent listener validates origin and performs navigation via
window.location.assign()
Message Format:
{
type: 'swf.widgets.search.navigate',
payload: {
url: 'https://booking-site.com/shopping?...', // Full search URL
reason: 'submit', // Always 'submit' for now
openInNewTab: false // true if Cmd/Ctrl+Click
}
}
Configuration Options:
useParentNavigation(boolean, default: auto-detect) - Force postMessage navigation even for non-iframe embedspostMessageOrigin(string, default:'*') - Target origin for postMessage (more secure than wildcard)
Example with Custom Origin:
SwitchflyWidgets.createSearchWidget({
containerId: 'search-widget',
targetUrl: 'https://your-booking-site.com',
useParentNavigation: true,
postMessageOrigin: 'https://your-parent-site.com'
});
URL Parameter Notes
- Colors must be URL-encoded (
#0a66c2→%230a66c2) cobrandparameter becomesnav=<value>in the final booking search URL- Only include parameters you want to override (all are optional)
- Destination prefill (
searchParam):- Text search:
searchParam=San%20Francisco- no label needed, resolves via typeahead API - Airport codes:
searchParam=|airport:LAX(URL-encoded:%7Cairport:LAX) - no label needed, resolves via typeahead - Numeric city IDs:
searchParam=|city:7321(URL-encoded:%7Ccity:7321) - REQUIRESprefillDestinationLabel=San%20Diego- Typeahead API cannot resolve numeric city IDs, so display label must be provided explicitly
- Both parameters are required for numeric IDs to prefill correctly
- Text search:
- Custom tabs:
- Supports up to 2 custom tabs with external URLs
- Custom tab URL is required for tab to render; label defaults to "Link" if omitted
- Position options:
first,last, orafter:<product>(e.g.,after:air) - Custom tabs open in new tab with
noopener,noreferrer - Custom tabs never become the active product tab
- Search date format:
- Use
searchDateFormatto control how dates are formatted in the Core deep-link URL (date1/date2 params) - Options:
mm/dd/yy(default),dd/mm/yy,yy/mm/dd,yyyy/mm/dd - This ONLY affects the URL format sent to the booking platform
- Does NOT change datepicker UI, on-screen date display, or locale-based formatting
- Use
For developers who need complete control over UI and want to build custom search interfaces, the Switchfly JavaScript SDK provides direct access to location APIs and URL building without using pre-built widgets.
When to Use the SDK
Pre-built widgets (iframe or script embed) cover most integration scenarios with minimal effort. Use the JavaScript SDK only when you need complete UI control — custom forms, custom styling, or deep integration with an existing framework or design system.
For a full comparison of all three integration approaches (iframe embed, script embed, and SDK), see the Choosing an Integration Method section below.
Installation
ES Modules (recommended):
import { createClient } from 'https://your-cdn.com/packages/sdk/src/index.js';
NPM (when published):
npm install @switchfly/sdk
import { createClient } from '@switchfly/sdk';
Quick Start Example (Hotel Search)
Complete working example showing location autocomplete and search URL generation:
<!DOCTYPE html>
<html>
<head>
<title>Custom Hotel Search</title>
</head>
<body>
<form id="hotelSearchForm">
<input type="text" id="destination" placeholder="Enter city" autocomplete="off" required>
<div id="autocompleteResults"></div>
<input type="date" id="checkin" required>
<input type="date" id="checkout" required>
<button type="submit">Search Hotels</button>
</form>
<script type="module">
import { createClient } from 'https://your-cdn.com/packages/sdk/src/index.js';
// Initialize SDK client
const sdk = createClient({
targetUrl: 'https://your-booking-site.com',
locale: 'en-US'
});
let selectedLocation = null;
let debounceTimer = null;
const destinationInput = document.getElementById('destination');
const autocompleteResults = document.getElementById('autocompleteResults');
const form = document.getElementById('hotelSearchForm');
// Location autocomplete
destinationInput.addEventListener('input', async (e) => {
const query = e.target.value.trim();
clearTimeout(debounceTimer);
selectedLocation = null;
if (query.length < 2) {
autocompleteResults.innerHTML = '';
return;
}
debounceTimer = setTimeout(async () => {
try {
const response = await sdk.locations.suggest({
query,
locationType: 'DESTINATION',
productTypes: 'ROOM' // Use 'ROOM' for hotel searches
});
const locations = response?.data?.locations || [];
// Render results
autocompleteResults.innerHTML = locations.map(loc => `
<div class="autocomplete-item" data-location='${JSON.stringify(loc)}'>
${loc.nameDisplayValue || loc.displayValue}
</div>
`).join('');
// Handle selection
autocompleteResults.querySelectorAll('.autocomplete-item').forEach(item => {
item.addEventListener('click', () => {
const loc = JSON.parse(item.dataset.location);
selectedLocation = loc;
destinationInput.value = loc.nameDisplayValue || loc.displayValue;
autocompleteResults.innerHTML = '';
});
});
} catch (error) {
console.error('Autocomplete error:', error);
}
}, 300);
});
// Form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!selectedLocation) {
alert('Please select a destination');
return;
}
// Build search URL
const searchInfo = {
type: 'HOTEL',
location: selectedLocation.search, // Location token like '|city:7321'
fromDate: document.getElementById('checkin').value,
toDate: document.getElementById('checkout').value,
rooms: [{ adults: 2, childAges: [] }]
};
const url = sdk.search.buildUrl(searchInfo);
console.log('Search URL:', url.href);
window.location.href = url.href; // Navigate to search results
});
</script>
</body>
</html>
API Reference
createClient(config)
Create an SDK client instance.
Parameters:
config.targetUrl(string, required): Target booking platform URL (e.g.,'https://your-booking-site.com')config.locale(string, optional): Locale string (e.g.,'en-US','fr','es','ja')config.cobrand(string, optional): Cobrand identifier for analytics/trackingconfig.dealsApiUrl(string, optional): Base URL for the deals service API gatewayconfig.apiKey(string, optional): API key for the deals service gateway (sent asx-api-keyheader)config.timeout(number, optional): Default request timeout in milliseconds (default:10000)config.headers(object, optional): Custom headers for API requestsconfig.fetch(function, optional): Custom fetch implementation
Returns: SDK client instance with locations, search, and utils modules
Example:
const sdk = createClient({
targetUrl: 'https://your-booking-site.com',
locale: 'en-US',
cobrand: 'partner123',
timeout: 15000
});
sdk.locations.suggest(options)
Search for location suggestions (autocomplete).
Parameters:
options.query(string, required): Search query (minimum 2 characters recommended)options.locationType(string, required): Location type'ORIGIN'- Departure locations (airports)'DESTINATION'- Arrival/destination locations (cities, airports, hotels)'ORIGIN_DESTINATION'- Both origin and destination
options.productTypes(string, required): Comma-separated product types'AIR'- Flights/airports'ROOM'- Hotels/accommodations (note: useROOM, notHOTEL)'CAR'- Car rentals'ACTIVITY'- Activities/experiences- Examples:
'AIR','ROOM','AIR,ROOM'
options.includeHotels(boolean, optional): Include individual hotel properties in resultsoptions.originLocation(string, optional): Origin location token for route-based filteringoptions.crsName(string, optional): Filter by CRS system nameoptions.provider(string, optional): Filter by provideroptions.signal(AbortSignal, optional): AbortSignal for request cancellation
Returns: Promise
