Switchfly-PrimaryLogo-Reversed-WEB
Switchfly Widgets Integration Guide

Switchfly Widgets Integration Guide (1.0.0)

Download OpenAPI specification:

Embeddable travel widgets for partner sites.

Available Widgets

  • Search Widget: Full-featured travel search (air, hotel, car, activities, bundles) Search Widget – Bundle (English)

  • Hotel Deals Widget: Showcase featured hotel deals with click-through to booking

Feature Overview

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, and Japanese. All UI text, labels, error messages, and validation prompts are automatically localized based on the locale parameter.

Search Widget – French Locale

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.

Search Widget – Custom Product Tabs

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 Widget – Car Search with Custom Branding (Spanish)

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.

Search Widget – Error Dialog

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).

Search Widget – Datepicker

Integration Guide

⚠️ 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.

  • targetUrl specifies your booking platform domain (e.g., https://your-booking-site.com)
  • Widgets use targetUrl as 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.iife.js">
    • Iframe embed: <iframe src="https://your-cdn.com/widgets/search/...">
  • 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)

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.

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.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',
    locale: 'en',
    maxItems: 6,
    showPrices: true,
    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>

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)
  • swf.widgets.search.error.dismissed - User dismissed error dialog

Parent → Widget Messages:

  • swf.widgets.search.configure - Inject error display (see Error Handling section)

Complete Parent Page Snippet (Iframe Embed):

var iframe = document.getElementById('search-widget-iframe');

// Parse ERROR_MESSAGE from URL on page load
var urlParams = new URLSearchParams(window.location.search);
var errorCode = urlParams.get('ERROR_MESSAGE');

// Send error to widget on load if present
if (errorCode) {
  iframe.addEventListener('load', function() {
    iframe.contentWindow.postMessage({
      type: 'swf.widgets.search.configure',
      payload: {
        error: {
          code: errorCode,
          clearOnDismiss: true
        }
      }
    }, '*');
  });
}

// 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);
      }
    }
  }

  // Clear error params when dismissed
  if (data && data.type === 'swf.widgets.search.error.dismissed') {
    var url = new URL(window.location);
    url.searchParams.delete('ERROR_MESSAGE');
    window.history.replaceState({}, '', url);
  }
});

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:

  1. Widget detects it's running in an iframe (window.self !== window.top)
  2. On search submit, widget sends swf.widgets.search.navigate message to parent
  3. 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 embeds
  • postMessageOrigin (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)
  • cobrand parameter becomes nav=<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) - REQUIRES prefillDestinationLabel=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
  • 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, or after:<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 searchDateFormat to 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

JavaScript SDK (Custom UI)

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 SDK vs Widgets

Use Pre-built Widgets when:

  • You want a ready-made, tested UI with minimal integration effort
  • Standard search functionality meets your needs
  • You prefer iframe or script embed without writing custom code
  • You need multi-product search (air, hotel, car, activities, bundles) out of the box

Use JavaScript SDK when:

  • You need complete UI control (custom forms, styling, frameworks)
  • Building a highly customized search experience
  • Integrating search into existing UI patterns or design systems
  • Want to compose search functionality with other features (e.g., combining with your own filters, recommendation engine, etc.)

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';

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/tracking
  • config.apiPathPrefix (string, optional): Custom API path prefix (default: '')
  • config.timeout (number, optional): Default request timeout in milliseconds (default: 10000)
  • config.headers (object, optional): Custom headers for API requests
  • config.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: use ROOM, not HOTEL)
    • 'CAR' - Car rentals
    • 'ACTIVITY' - Activities/experiences
    • Examples: 'AIR', 'ROOM', 'AIR,ROOM'
  • options.includeHotels (boolean, optional): Include individual hotel properties in results
  • options.originLocation (string, optional): Origin location token for route-based filtering
  • options.crsName (string, optional): Filter by CRS system name
  • options.provider (string, optional): Filter by provider
  • options.signal (AbortSignal, optional): AbortSignal for request cancellation

Returns: Promise with structure:

{
  data: {
    locations: [
      {
        nameDisplayValue: "San Diego, California, United States",
        displayValue: "San Diego, California, United States",
        locationType: "CITY",
        search: "|city:7321"  // Use this token for search URLs
      }
    ]
  }
}

Example:

const results = await sdk.locations.suggest({
  query: 'san diego',
  locationType: 'DESTINATION',
  productTypes: 'ROOM'
});

const locations = results.data.locations;
locations.forEach(loc => {
  console.log(loc.nameDisplayValue, loc.search);
});

sdk.locations.resolve(options)

Resolve a prefilled location token to a full location object. Useful when you have a location token (e.g., from URL params) and need display information.

Parameters:

  • options.searchParam (string, required): Location token (e.g., '|city:7321', '|airport:LAX')
  • options.prefillLabel (string, optional): Display label for numeric city IDs (required for numeric IDs since typeahead API cannot resolve them)
  • options.locationType (string, required): Location type ('ORIGIN', 'DESTINATION', 'ORIGIN_DESTINATION')
  • options.productTypes (string, required): Product types (e.g., 'ROOM', 'AIR')

Returns: Promise<Object|null> - Resolved location object or null if not found

Example:

// Resolve airport code (works without label)
const location = await sdk.locations.resolve({
  searchParam: '|airport:LAX',
  locationType: 'DESTINATION',
  productTypes: 'ROOM'
});

// Resolve numeric city ID (requires label)
const location = await sdk.locations.resolve({
  searchParam: '|city:7321',
  prefillLabel: 'San Diego',
  locationType: 'DESTINATION',
  productTypes: 'ROOM'
});

sdk.search.buildUrl(searchInfo, config?)

Build a shopping platform deep-link URL for search results.

Parameters:

searchInfo object (required):

  • searchInfo.type (string, required): Product type
    • 'AIR' - Flights only
    • 'HOTEL' - Hotels only
    • 'CAR' - Car rentals only
    • 'ACTIVITY' - Activities only
    • 'BUNDLE' - Multi-product bundles (requires bundleTypes)
  • searchInfo.bundleTypes (array, required if type='BUNDLE'): Array of bundle products (e.g., ['AIR', 'HOTEL'])
  • searchInfo.location (string, required): Destination location token from locations.suggest() (e.g., '|city:7321')
  • searchInfo.fromDate (string|Date, required): Start/check-in date (ISO format: '2026-03-15' or Date object)
  • searchInfo.toDate (string|Date, optional): End/check-out date (required for hotel, optional for one-way flights)
  • searchInfo.fromLocation (string, required for AIR/CAR): Origin location token (e.g., '|airport:LAX')

Traveler Configuration (Hotels/Bundles):

  • searchInfo.rooms (array, optional): Room configurations
    rooms: [
      { adults: 2, childAges: [8, 5] },  // Room 1: 2 adults, 2 children
      { adults: 1, childAges: [] }        // Room 2: 1 adult
    ]
    

Traveler Configuration (Air/Car/Activities - flat structure):

  • searchInfo.numAdults (number, optional): Number of adults (default: 2)
  • searchInfo.childAges (array, optional): Array of child ages (e.g., [8, 5])

Air-specific:

  • searchInfo.serviceClass (string, optional): 'coach', 'premiumeconomy', 'business', 'first' (default: 'coach')
  • searchInfo.roundtrip (boolean, optional): true for roundtrip, false for one-way (default: true)
  • searchInfo.airline (string, optional): Airline code filter (e.g., 'UA')
  • searchInfo.directOnly (boolean, optional): Direct/non-stop flights only

Car-specific:

  • searchInfo.fromTime (string, optional): Pick-up time in 24hr format (e.g., '14' for 2pm, default: '12')
  • searchInfo.toTime (string, optional): Drop-off time in 24hr format (default: '12')
  • searchInfo.driverAgeDefault (boolean, optional): Use default driver age
  • searchInfo.driverAge (number, optional): Specific driver age

config object (optional): Additional parameters for analytics/tracking

  • Any additional properties are passed through as URL parameters

Returns: URL object (use .href property for full URL string)

Examples:

Hotel Search:

const url = sdk.search.buildUrl({
  type: 'HOTEL',
  location: '|city:7321',
  fromDate: '2026-03-15',
  toDate: '2026-03-20',
  rooms: [
    { adults: 2, childAges: [8, 5] }
  ]
});

window.location.href = url.href;

Air Search (Roundtrip):

const url = sdk.search.buildUrl({
  type: 'AIR',
  fromLocation: '|airport:LAX',
  location: '|airport:JFK',
  fromDate: '2026-03-15',
  toDate: '2026-03-22',
  numAdults: 2,
  childAges: [8],
  serviceClass: 'business',
  directOnly: true
});

Bundle Search (Flight + Hotel):

const url = sdk.search.buildUrl({
  type: 'BUNDLE',
  bundleTypes: ['AIR', 'HOTEL'],
  fromLocation: '|airport:LAX',
  location: '|city:7321',
  fromDate: '2026-03-15',
  toDate: '2026-03-20',
  rooms: [
    { adults: 2, childAges: [] }
  ],
  serviceClass: 'coach'
});

Car Rental:

const url = sdk.search.buildUrl({
  type: 'CAR',
  fromLocation: '|airport:LAX',
  location: '|airport:LAX',  // Same location for local rental
  fromDate: '2026-03-15',
  toDate: '2026-03-20',
  fromTime: '10',  // 10am pickup
  toTime: '14',    // 2pm dropoff
  driverAge: 25
});

sdk.locations.clearCache(options?)

Clear location autocomplete cache.

Parameters:

  • options.locale (string, optional): Clear specific locale only
  • options.locationType (string, optional): Clear specific location type only

Example:

// Clear all cache
sdk.locations.clearCache();

// Clear only French locale
sdk.locations.clearCache({ locale: 'fr' });

// Clear only destination cache
sdk.locations.clearCache({ locationType: 'DESTINATION' });

sdk.locations.getCacheStats()

Get cache statistics for debugging/monitoring.

Returns: Object with cache statistics

{
  entries: 42,
  sizeBytes: 125000,
  enabled: true
}

sdk.utils.parseSearchToken(token)

Parse a location search token into its components.

Parameters:

  • token (string): Location token (e.g., '|city:7321', '|airport:LAX', '7321')

Returns: Object with parsed components

{
  type: 'city',           // 'city', 'airport', or 'numeric'
  value: '7321',          // The identifier value
  canonical: '|city:7321' // Canonical format with pipe prefix
}

Example:

const parsed = sdk.utils.parseSearchToken('|city:7321');
console.log(parsed.type);      // 'city'
console.log(parsed.value);     // '7321'
console.log(parsed.canonical); // '|city:7321'

sdk.utils.debounce(fn, delay)

Create a debounced function (useful for autocomplete).

Parameters:

  • fn (function): Function to debounce
  • delay (number): Delay in milliseconds

Returns: Debounced function

Example:

const debouncedSearch = sdk.utils.debounce(async (query) => {
  const results = await sdk.locations.suggest({
    query,
    locationType: 'DESTINATION',
    productTypes: 'ROOM'
  });
  // Handle results...
}, 300);

input.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

SDK Notes

  • Location tokens: Always use the search property from location objects (e.g., '|city:7321', '|airport:LAX') when building search URLs. These are the canonical identifiers expected by the booking platform.
  • Product type naming: Use 'ROOM' (not 'HOTEL') for hotel searches in locations.suggest(). The API expects ROOM as the product type.
  • Numeric city IDs: Cannot be resolved via typeahead API without a prefillLabel. If you have a numeric city token like '|city:7321', you must provide the display label separately.
  • Date formats: Dates can be provided as ISO strings ('2026-03-15') or Date objects. The SDK handles formatting for the booking platform.
  • Error handling: All async methods (suggest, resolve, buildUrl) can throw errors. Always wrap in try/catch blocks.
  • Caching: Location suggestions are cached in localStorage with a 30-day TTL to reduce API calls and improve performance.

AI Developer Tools

The Switchfly SDK ships AI assistant skills to accelerate widget integration. These are prompt workflows that guide developers through configuration interactively and output a ready-to-paste embed snippet — no manual parameter lookup required.

Available Skills

Skill Tool Command
Search Widget Setup Claude Code /swf-search-widget
Search Widget Setup Cursor @swf-search-widget

Each skill walks through required and optional configuration one step at a time, then generates a complete, copy-paste-ready HTML snippet.

Installation

Claude Code:

cp node_modules/@switchfly/sdk/skills/swf-search-widget/SKILL.md .claude/skills/swf-search-widget/SKILL.md

Cursor:

cp node_modules/@switchfly/sdk/skills/swf-search-widget/cursor.mdc .cursor/rules/swf-search-widget.mdc

Usage

Claude Code — run in your project:

/swf-search-widget

Or pre-populate known values to skip prompts:

/swf-search-widget targetUrl=https://booking.yourairline.com locale=ja cobrand=partner123

Cursor — reference in chat or Composer:

@swf-search-widget

The skill prompts for any missing required fields, asks clarifying questions for optional configuration, and outputs a complete snippet with only the values you specified.

Embed Methods

Switchfly widgets support two integration methods:

  • Iframe embed (recommended): simplest integration and maximum isolation from host page styles/scripts.
  • Direct script embed (IIFE): renders inline on the host page for tighter visual integration.

Both methods are supported out of the gate. Hosting of the widget IIFE and the API proxy approach will be documented once finalized.

Generated Search URLs

When users submit a search, the widget generates a deep-link URL to the booking platform with search parameters.

Traveler Representation: All search URLs use room_info as the canonical traveler data format:

  • room_info is a JSON payload containing rooms, adults, children, child ages, seniors, and infants
  • The JSON is URL-encoded before being added to the search URL
  • Example structure: {"rooms":[{"numAdults":2,"numSeniors":0,"numInfantsInSeats":0,"numInfantsInLaps":0,"childAges":[8,12]}]}
  • Legacy traveler parameters (adults, children, child_ageN, infantN) are no longer used

Product Type Flags: Search URLs also include product type flags to indicate the search intent:

  • air1=true - Air search
  • room1=true - Hotel search
  • car1=true - Car search
  • activity1=true - Activity search

Other Parameters: See individual widget configuration parameters for marketing/tracking parameters (discount_code, utm_source, etc.)

Error Handling (PostMessage API)

When a search fails on the booking platform, the user may be redirected back to the host page with an error parameter (e.g., ?ERROR_MESSAGE=NO_ROOMS_FOUND). The widget provides a postMessage API for the host page to inject and display these errors.

Flow Overview

  1. Parent page detects error: Parse ERROR_MESSAGE from the URL query string
  2. Parent sends error to widget: Use postMessage to send a swf.widgets.search.configure message with error payload
  3. Widget displays error: Shows error message in a modal dialog using existing UI components
  4. User dismisses error: Widget sends swf.widgets.search.error.dismissed message back to parent
  5. Parent clears error: Remove ERROR_MESSAGE from URL using history.replaceState()

Message Types

swf.widgets.search.configure (Parent → Widget)

Inject configuration including optional error display.

window.frames[0].postMessage({
  type: 'swf.widgets.search.configure',
  payload: {
    error: {
      code: 'NO_ROOMS_FOUND',          // Required: error code
      searchUrl: 'https://...',         // Optional: reconstructed search URL
      clearOnDismiss: true              // Optional: whether to notify parent (default: true)
    }
  }
}, '*');

swf.widgets.search.error.dismissed (Widget → Parent)

Notification that the user dismissed the error dialog.

window.addEventListener('message', function(event) {
  if (event.data.type === 'swf.widgets.search.error.dismissed') {
    const errorCode = event.data.payload.code;
    // Clear ERROR_MESSAGE from URL
    const url = new URL(window.location);
    url.searchParams.delete('ERROR_MESSAGE');
    window.history.replaceState({}, '', url);
  }
});

Supported Error Codes

The widget maps error codes to localized user-facing messages. If an unknown code is provided, the DEFAULT message is shown.

Error Code English Message
DATE_TOO_FAR_IN_ADVANCE "The date is too far in advance. Please try again."
DEFAULT "An unexpected error has occurred. Please try your search again."
INTERNAL_ERROR "An unexpected error has occurred. Please try your search again."
SESSION_ERROR "Your session has encountered an error. Please try your search again."
THIRD_PARTY_ERROR "We are having problems communicating with our suppliers. Please try your search again."
NO_FLIGHTS_FOUND "No available flights were found. Please adjust your search and try again."
NO_ROOMS_FOUND "There are no rooms available during the dates you have selected. Please try different dates or destination."
NO_CARS_FOUND "There are no cars available during the dates you have selected. Please try different dates or destination."
NO_ACTIVITIES_FOUND "There are no activities available during the dates you have selected. Please try different dates or destination."
SEARCH_LOCATION_NOT_FOUND "The search location was not found."

All error messages are translated for all widget-supported languages (en, fr, es, ja).

Parent Page Implementation Example

<iframe id="search-widget-iframe" src="https://your-cdn.com/widgets/search/" width="100%" height="600"></iframe>

<script>
  // On page load, check for ERROR_MESSAGE parameter
  const urlParams = new URLSearchParams(window.location.search);
  const errorCode = urlParams.get('ERROR_MESSAGE');

  if (errorCode) {
    // Wait for iframe to load
    const iframe = document.getElementById('search-widget-iframe');
    iframe.addEventListener('load', function() {
      // Send error to widget
      iframe.contentWindow.postMessage({
        type: 'swf.widgets.search.configure',
        payload: {
          error: {
            code: errorCode,
            clearOnDismiss: true
          }
        }
      }, '*');
    });
  }

  // Listen for error dismissal
  window.addEventListener('message', function(event) {
    if (event.data.type === 'swf.widgets.search.error.dismissed') {
      // Clear ERROR_MESSAGE from URL
      const url = new URL(window.location);
      url.searchParams.delete('ERROR_MESSAGE');
      window.history.replaceState({}, '', url);
    }
  });
</script>

Static Locations Mode

Replace the default Location API autocomplete with fixed location lists from your own JSON data. Useful for airlines with restricted route networks or when you want to limit search options to specific airports/cities.

Display Modes

Static locations support two display modes:

  • dropdown (default): Custom dropdown showing all items with icons. Best for small lists (5-15 items). Users click to open and scroll to find their location.

  • typeahead: Autocomplete-style input with client-side filtering. Best for larger lists (20+ items). Users type to filter locations instantly (no network calls).

Script Embed with JSON URL

<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
  SwitchflyWidgets.createSearchWidget({
    containerId: 'search-widget',
    targetUrl: 'https://your-booking-site.com',
    staticLocationsUrl: 'https://cdn.example.com/locations.json',
    staticLocationsDisplayMode: 'typeahead'  // Optional: 'dropdown' (default) or 'typeahead'
  });
</script>

Script Embed with Inline JSON

<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
  SwitchflyWidgets.createSearchWidget({
    containerId: 'search-widget',
    targetUrl: 'https://your-booking-site.com',
    staticLocationsDisplayMode: 'dropdown',  // Optional: 'dropdown' or 'typeahead'
    staticLocations: {
      origins: [
        { displayValue: "Tokyo (TYO)", search: "|airport:TYO", locationType: "AIRPORT" },
        { displayValue: "Osaka", search: "|city:27562", locationType: "CITY" }
      ],
      destinations: [
        { displayValue: "Los Angeles (LAX)", search: "|airport:LAX", locationType: "AIRPORT" },
        { displayValue: "San Francisco (SFO)", search: "|airport:SFO", locationType: "AIRPORT" }
      ]
    }
  });
</script>

Iframe Embed with JSON URL

<iframe
  src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com&staticLocationsUrl=https%3A%2F%2Fcdn.example.com%2Flocations.json&staticLocationsDisplayMode=typeahead"
  width="100%"
  height="600"
  frameborder="0">
</iframe>

Note: staticLocationsDisplayMode is optional. Defaults to dropdown if omitted.

Iframe Embed with postMessage (Dynamic)

<iframe id="search-widget" src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com" width="100%" height="600" frameborder="0"></iframe>
<script>
  window.addEventListener('load', () => {
    document.getElementById('search-widget').contentWindow.postMessage({
      type: 'swf.widgets.search.configure',
      payload: {
        staticLocationsDisplayMode: 'typeahead',  // Optional: 'dropdown' or 'typeahead'
        staticLocations: {
          origins: [
            { displayValue: "Tokyo (TYO)", search: "|airport:TYO", locationType: "AIRPORT" }
          ],
          destinations: [
            { displayValue: "Los Angeles (LAX)", search: "|airport:LAX", locationType: "AIRPORT" }
          ]
        }
      }
    }, 'https://your-cdn.com');
  });
</script>

JSON Format

{
  "origins": [
    {
      "displayValue": "Tokyo Haneda (HND)",
      "search": "|airport:HND",
      "locationType": "AIRPORT"
    },
    {
      "displayValue": "Osaka",
      "search": "|city:27562",
      "locationType": "CITY"
    }
  ],
  "destinations": [
    {
      "displayValue": "Los Angeles (LAX)",
      "search": "|airport:LAX",
      "locationType": "AIRPORT"
    }
  ]
}

Field Descriptions:

  • origins: Array of origin locations (optional - if omitted, origin field uses autocomplete)
  • destinations: Array of destination locations (optional - if omitted, destination field uses autocomplete)
  • displayValue: Text shown to user (e.g., "Tokyo (TYO)" or "Osaka")
  • search: Search token sent to Core Shopping (format: "|airport:CODE" or "|city:ID")
  • locationType: Must be "AIRPORT" or "CITY" (determines icon shown)

Display Mode Behavior:

  • dropdown (default): Custom dropdown with SVG icons. Shows all items at once. Best for small lists (5-15 items).
  • typeahead: Autocomplete-style input with client-side filtering. User types to filter. Best for larger lists (20+ items). No network calls - instant filtering.

Notes:

  • You can provide only origins, only destinations, or both
  • Fields not in staticLocations fall back to autocomplete
  • For car rentals, both pickup and dropoff use destinations array
  • Invalid location items are logged to console and skipped
  • Display mode applies to all static location fields in the widget
  • Static locations are NOT filtered by product type - The same location list is shown for all product types (air, hotel, car, activity, bundle). This differs from API autocomplete mode, which filters results based on product type. Ensure your static locations are appropriate for all product types you're offering, or consider using API autocomplete for product-specific filtering.

Hotel Deep-Linking

Feature a specific hotel on search results or redirect directly to hotel details page. Useful for marketing campaigns, email newsletters, or partner sites promoting individual properties.

Feature Hotel on Search Results

Pass hotel_id to highlight/feature a specific hotel in search results:

Script Embed:

<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
  SwitchflyWidgets.createSearchWidget({
    containerId: 'search-widget',
    targetUrl: 'https://your-booking-site.com',
    hotel_id: '12345'
  });
</script>

Iframe Embed:

<iframe
  src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com&hotel_id=12345"
  width="100%"
  height="600"
  frameborder="0">
</iframe>

Redirect to Hotel Details Page

Combine hotel_id with redirect_to_hotel_details to bypass search results and go directly to the hotel details page:

Script Embed:

<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
  SwitchflyWidgets.createSearchWidget({
    containerId: 'search-widget',
    targetUrl: 'https://your-booking-site.com',
    hotel_id: '12345',
    redirect_to_hotel_details: true
  });
</script>

Iframe Embed:

<iframe
  src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com&hotel_id=12345&redirect_to_hotel_details=true"
  width="100%"
  height="600"
  frameborder="0">
</iframe>

Notes:

  • hotel_id is passed through to the booking URL as hotel_id parameter
  • redirect_to_hotel_details only works when hotel_id is also provided
  • These parameters work for all search types (not just hotel searches)
  • The hotel ID format should match your booking platform's hotel identifier format

Activity Deep-Linking

Feature a specific activity on search results or redirect directly to activity details page. Activities require both an activity ID and a CRS (provider/system identifier) such as "viator", "expedia", "getyourguide", etc.

Feature Activity on Search Results

Pass activity_id and activity_crs to highlight/feature a specific activity in search results:

Script Embed:

<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
  SwitchflyWidgets.createSearchWidget({
    containerId: 'search-widget',
    targetUrl: 'https://your-booking-site.com',
    activity_id: '12345',
    activity_crs: 'viator'
  });
</script>

Iframe Embed:

<iframe
  src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com&activity_id=12345%3Aviator"
  width="100%"
  height="600"
  frameborder="0">
</iframe>

Redirect to Activity Details Page

Combine activity_id, activity_crs, and redirect_to_activity_details to bypass search results and go directly to the activity details page:

Script Embed:

<div id="search-widget"></div>
<script src="https://your-cdn.com/dist/switchfly-widgets.iife.js"></script>
<script>
  SwitchflyWidgets.createSearchWidget({
    containerId: 'search-widget',
    targetUrl: 'https://your-booking-site.com',
    activity_id: '12345',
    activity_crs: 'viator',
    redirect_to_activity_details: true
  });
</script>

Iframe Embed:

<iframe
  src="https://your-cdn.com/widgets/search/?targetUrl=https%3A%2F%2Fyour-booking-site.com&activity_id=12345%3Aviator&redirect_to_activity_details=true"
  width="100%"
  height="600"
  frameborder="0">
</iframe>

Notes:

  • CRS is required: Unlike hotels, activities always require a CRS (provider identifier)
  • In iframe mode, combine ID and CRS using colon format: activity_id=12345:viator
  • In script embed mode, provide activity_id and activity_crs as separate properties
  • activity_id is passed through to the booking URL as activity_id parameter in id:crs format
  • redirect_to_activity_details only works when activity_id is also provided
  • These parameters work for all search types (not just activity searches)
  • Common CRS values: "viator", "expedia", "getyourguide", etc.

CMS Integration Notes

The notes below call out CMS-specific considerations. Unless stated otherwise, iframe embed is the recommended approach.

HubSpot

  • Recommended: Iframe embed in a Custom HTML module
  • Supported: Direct script embed (subject to HubSpot script/CSP restrictions)
  • Allowlist the widget host domain (for example, your-cdn.com) in Content Security Policy (CSP) if needed
  • Listen for SWF_WIDGET_HEIGHT to resize iframe dynamically
  • URL-encode all color parameters (#%23)

Wix

  • Recommended: Iframe embed via HTML Embed or Custom Element
  • Supported: Direct script embed
  • For iframe: Set an explicit height (for example, 600px) or use an auto-height script
  • Ensure the iframe src domain (or script source domain) is allowlisted
  • URL-encode all color parameters (#%23)

Webflow

  • Recommended: Iframe embed via Custom Code component
  • Supported: Direct script embed
  • For iframe: Set an explicit height or add a resize listener via postMessage
  • URL-encode all color parameters (#%23)

Squarespace

  • Recommended: Iframe embed using a Code Block
  • Supported: Direct script embed
  • For iframe: Set an explicit height (Squarespace does not support dynamic iframe resizing)
  • Ensure the iframe src domain (or script source domain) is allowlisted
  • URL-encode all color parameters (#%23)

WordPress

  • Recommended: Iframe embed using Custom HTML block
  • Supported: Direct script embed
  • For script embeds, some themes or security plugins may block inline scripts
  • For iframe: Set an explicit height or handle resizing via postMessage
  • URL-encode all color parameters (#%23)

Shopify

  • Recommended: Iframe embed using Custom HTML section or Liquid template
  • Supported: Direct script embed
  • Script embeds may require adding the widget domain to Shopify’s allowed script sources
  • For iframe: Set an explicit height or implement postMessage-based resizing
  • URL-encode all color parameters (#%23)

search-widget

Travel search widget

Search Widget HTML (iframe entrypoint)

Serves the search widget HTML page for iframe embedding.

All parameters are optional and override widget defaults.

Example with branding:

/widgets/search/?locale=fr&cobrand=partner123&accentColor=%230a66c2&bgColorWidget=%23ffffff

Example with destination prefill (numeric city ID):

/widgets/search/?searchParam=%7Ccity:7321&prefillDestinationLabel=San%20Diego

Example with custom tabs:

/widgets/search/?customTab1Label=Cruises&customTab1Url=https://example.com/cruises&customTab1Position=last
query Parameters
locale
string
Example: locale=en

Widget locale (e.g., en, es, fr, ja)

searchParam
string
Example: searchParam=%7Cairport:LAS

Pre-fill destination search token (format: "|airport:CODE" or "|city:ID").

Examples:

  • Airport: "|airport:LAS" (URL-encoded: "%7Cairport:LAS")
  • City: "|city:7321" (URL-encoded: "%7Ccity:7321")

For numeric city IDs, also provide prefillDestinationLabel since the typeahead API cannot resolve numeric IDs.

prefillDestinationLabel
string
Example: prefillDestinationLabel=San Diego

Optional display label for pre-filled destination.

Required when using numeric city ID tokens (e.g., "|city:7321") because the Locations typeahead API cannot resolve numeric IDs. The widget will display this label in the destination input while sending the canonical token to Core Shopping on submit.

Not needed for airport codes (e.g., "|airport:LAS") as those resolve via typeahead API.

cobrand
string
Example: cobrand=partner123

Cobrand identifier (becomes nav= in booking URL)

searchDateFormat
string
Default: "mm/dd/yy"
Enum: "mm/dd/yy" "dd/mm/yy" "yy/mm/dd" "yyyy/mm/dd"
Example: searchDateFormat=dd/mm/yy

Date format for Core deep-link URL (date1/date2 query params).

This only affects the format of dates sent to the booking platform—it does NOT change the datepicker UI or any on-screen date display.

Options: mm/dd/yy, dd/mm/yy, yy/mm/dd, yyyy/mm/dd

availableProducts
string
Example: availableProducts=bundle,air,hotel

Comma-separated list of available product types for the search widget tab set.

Valid values: bundle, air, hotel, car, activity

Order matters—tabs will render in the order specified. Invalid values are ignored. If all values are invalid or parameter is empty, falls back to default: bundle,air,hotel,car,activity

Bundle Tab Behavior: When the bundle tab is active, this parameter also controls which bundle package pills are displayed. For example, if availableProducts excludes "car", then bundle pills like "Flight+Hotel+Car" and "Hotel+Car" will be hidden. Only bundle combinations using available products will be shown.

defaultProductType
string
Enum: "bundle" "air" "hotel" "car" "activity"
Example: defaultProductType=bundle

Default product type to display on widget load.

Must be one of the values in availableProducts. If invalid or not in availableProducts list, defaults to first item in availableProducts.

customTab1Label
string
Example: customTab1Label=Cruises

Label for first custom tab (e.g., "Cruises")

customTab1Url
string
Example: customTab1Url=https://example.com/cruises

URL for first custom tab. Required for tab to render. Opens in new tab when clicked with noopener and noreferrer.

customTab1Position
string
Default: "last"
Enum: "first" "last" "after:bundle" "after:air" "after:hotel" "after:car" "after:activity"
Example: customTab1Position=last

Position for first custom tab in tab row. Options: "first", "last", "after:bundle", "after:air", "after:hotel", "after:car", "after:activity"

customTab2Label
string
Example: customTab2Label=Deals

Label for second custom tab

customTab2Url
string
Example: customTab2Url=https://example.com/deals

URL for second custom tab. Required for tab to render. Opens in new tab when clicked with noopener and noreferrer.

customTab2Position
string
Default: "last"
Enum: "first" "last" "after:bundle" "after:air" "after:hotel" "after:car" "after:activity"
Example: customTab2Position=after:air

Position for second custom tab in tab row. Options: "first", "last", "after:bundle", "after:air", "after:hotel", "after:car", "after:activity"

discountCode
string
Example: discountCode=SUMMER2026

Discount code for promotional pricing. Passed through to booking URL as discount_code parameter.

currency
string^[A-Z]{3}$
Example: currency=USD

Currency code for price display (3-letter ISO 4217 code, e.g., USD, EUR, GBP). Passed through to booking URL as display_currency parameter. Must be exactly 3 uppercase letters A-Z; invalid values are ignored.

languageId
integer
Example: languageId=3

Numeric language identifier for booking platform. Passed through to booking URL as langid parameter. Must be a valid integer; invalid values are ignored.

utmSource
string
Example: utmSource=newsletter

UTM source parameter for campaign tracking. Passed through to booking URL as utm_source parameter.

utmMedium
string
Example: utmMedium=email

UTM medium parameter for campaign tracking. Passed through to booking URL as utm_medium parameter.

utmCampaign
string
Example: utmCampaign=summer_sale_2026

UTM campaign parameter for campaign tracking. Passed through to booking URL as utm_campaign parameter.

utmContent
string
Example: utmContent=hero_banner

UTM content parameter for A/B testing and content differentiation. Passed through to booking URL as utm_content parameter.

trackId
integer
Example: trackId=1234

The agent ID of the referring agent. Passed through to booking URL as track parameter. Must be a valid integer; invalid values are ignored.

bgColorWidget
string
Example: bgColorWidget=%23ffffff

Background color (URL-encoded)

highlightColor
string
Example: highlightColor=%23eef2ff

Highlight color for focus/hover states (URL-encoded)

accentColor
string
Example: accentColor=%230a66c2

Primary accent color (URL-encoded)

accentColorHover
string
Example: accentColorHover=%23004182

Accent hover color (URL-encoded)

accentColorActive
string
Example: accentColorActive=%23003366

Accent active/pressed color (URL-encoded)

Responses

hotel-deals-widget

Hotel deals showcase widget

Hotel Deals Widget HTML (iframe entrypoint)

Serves the hotel deals widget HTML page for iframe embedding.

All parameters are optional and override widget defaults.

Example with branding:

/widgets/hotelDeals/?locale=en&maxItems=6&accentColor=%23dc2626
query Parameters
locale
string
Example: locale=en

Widget locale (e.g., en, es, fr, ja)

maxItems
integer [ 1 .. 12 ]
Example: maxItems=6

Maximum number of hotels to display (1-12)

showPrices
boolean
Example: showPrices=true

Display hotel prices in tiles

currency
string^[A-Z]{3}$
Example: currency=USD

Currency code for price display (3-letter ISO 4217 code, e.g., USD, EUR, GBP). Passed through to booking URL as display_currency parameter. Must be exactly 3 uppercase letters A-Z; invalid values are ignored.

title
string
Example: title=Featured Hotels

Widget title heading

cobrand
string
Example: cobrand=partner123

Cobrand identifier (becomes nav= in booking URL)

searchParam
string
Example: searchParam=%7Cairport:LAS

Pre-fill destination search token (format: "|airport:CODE" or "|city:ID").

Examples:

  • Airport: "|airport:LAS" (URL-encoded: "%7Cairport:LAS")
  • City: "|city:7321" (URL-encoded: "%7Ccity:7321")

For numeric city IDs, also provide prefillDestinationLabel since the typeahead API cannot resolve numeric IDs.

bgColorWidget
string
Example: bgColorWidget=%23ffffff

Background color (URL-encoded)

highlightColor
string
Example: highlightColor=%23eef2ff

Highlight color for focus/hover states (URL-encoded)

accentColor
string
Example: accentColor=%230a66c2

Primary accent color (URL-encoded)

accentColorHover
string
Example: accentColorHover=%23004182

Accent hover color (URL-encoded)

accentColorActive
string
Example: accentColorActive=%23003366

Accent active/pressed color (URL-encoded)

Responses