Widget Embedder's Guide

A developer's guide to embedding OptionsPlay® HTML5 widgets into your platform.

Quick Start

Get a widget running in under 5 minutes.

Prerequisites

Before you begin, you'll need:

Item Description How to Get
Widget Loader Endpoint Environment-specific URL for the widget loader script Provided by OptionsPlay support team
API Key Your public key for client-side authentication Provided by OptionsPlay administrator
Whitelisted Domain The domain(s) where widgets will be embedded Submit to OptionsPlay during provisioning
User ID Strategy How you'll uniquely identify your users Your choice - see User Identification
📋 Note

Throughout this guide, {{WIDGET_LOADER_ENDPOINT}} represents your environment-specific endpoint URL. Replace this placeholder with the actual URL provided by the OptionsPlay support team for your environment (e.g., production, staging, sandbox).

Minimal Implementation

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>OptionsPlay Widget</title>
</head>
<body>
  <!-- 1. Container for the widget -->
  <div id="widget-container" style="width: 100%; height: 800px;"></div>

  <!-- 2. Load the widget loader -->
  <script src="https://{{WIDGET_LOADER_ENDPOINT}}/widgetLoader"></script>

  <!-- 3. Initialize the widget -->
  <script>
    const options = {
      destinationId: 'widget-container',
      apiKey: 'YOUR_API_KEY',
      userId: 'unique-user-id',
      width: '100%',
      height: '800px',
      theme: 'light',
      symbol: 'AAPL'
    };

    const widget = optionsPlayWidgetLoader.load('pro', options);
  </script>
</body>
</html>

That's it. The widget will authenticate, load, and display options analysis for AAPL.

Available Widgets

Widget Type Value Description
Ideas 'pro' Full OptionsPlay® Ideas experience
Strategies 'proStrategies' Strategies-focused widget

Authentication

OptionsPlay supports two authentication flows. Choose based on your environment:

Flow Use When Security Level
API Key Web apps with known domains Standard
Server-Side Token Desktop apps, mobile webviews, or when domain validation isn't possible Enhanced

API Key Authentication (Client-Side)

The simplest approach for web applications. Authentication uses your API key combined with domain whitelisting.

How It Works

sequenceDiagram autonumber participant Embedder as Embedder Platform participant Browser as User Web Browser participant OP as Widgets Provider
(OptionsPlay) Embedder->>Browser: Web page with OP Widget DOM Browser->>OP: GET /widgetLoader OP-->>Browser: JavaScript SDK Note over Browser: load(widgetName, options)
options includes apiKey + userId Browser->>OP: POST /Token with auth_token alt Request Valid OP-->>Browser: OAuth bearer access_token Note over OP: access_token valid for 1 day Browser->>Browser: Add iframe Browser->>OP: POST /setCookie with access_token OP->>OP: Validate access_token and Host OP-->>Browser: session_cookie returned Note over Browser,OP: All subsequent requests use session_cookie Browser-->>OP: SignalR connection established OP-->>Browser: Real-time symbol & market data else Invalid Credentials OP-->>Browser: 401 Unauthorized end

API Key Authentication Flow

Implementation

const options = {
  destinationId: 'widget-container',
  apiKey: 'YOUR_API_KEY',           // From OptionsPlay admin
  userId: 'unique-user-identifier', // Your user's unique ID
  width: '100%',
  height: '800px',
  theme: 'light'
};

const widget = optionsPlayWidgetLoader.load('pro', options);

Widget Cleanup

Always destroy the widget when the user navigates away to properly clean up server sessions:

// When user leaves the page or you need to unload the widget
widget.destroyWidget();
sequenceDiagram participant Browser as User Web Browser participant OP as Widgets Provider Note over Browser: User navigates away or widget unloaded Browser->>Browser: widget.destroyWidget() Browser->>OP: POST /killSession OP->>OP: access_token marked deleted OP-->>Browser: session destroyed message Note over OP: session_cookie removed Browser->>Browser: Destroy iframe

Widget Cleanup Flow

Server-Side Token Authentication

Use this flow when:

  • Your app runs in a browser-less environment (desktop app, mobile webview)
  • You can't rely on domain validation
  • You need tighter security control

How It Works

sequenceDiagram autonumber participant Embedder as Embedder Platform participant Browser as User Web Browser participant OP as Widgets Provider
(OptionsPlay) rect rgb(255, 250, 240) Note over Embedder,OP: Step 1-2: Server-to-Server Token Request Embedder->>OP: POST /Auth
apiKey + secretKey Note over OP: Validate credentials OP-->>Embedder: one-time auth_token end Embedder->>Browser: Web page with OP Widget DOM Browser->>OP: GET /widgetLoader OP-->>Browser: JavaScript SDK rect rgb(240, 255, 240) Note over Browser: Step 3: Load with authToken
(instead of apiKey) end Browser->>OP: POST /Token with auth_token Note over OP: auth_token marked expired
(one-time use) alt Token Valid OP-->>Browser: OAuth bearer access_token Note over OP: access_token valid for 1 day Browser->>Browser: Add iframe Browser->>OP: POST /setCookie with access_token OP->>OP: Validate access_token and Host OP-->>Browser: session_cookie returned Browser-->>OP: SignalR connection established OP-->>Browser: Real-time symbol & market data else Invalid/Expired Token OP-->>Browser: 401 Unauthorized end

Server-Side Token Authentication Flow

Server-Side Implementation

public async Task<string> GetAuthTokenAsync()
{
    using var client = new HttpClient();
    
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("apikey", "YOUR_API_KEY"),
        new KeyValuePair<string, string>("secretKey", "YOUR_SECRET_KEY")
    });
    
    var response = await client.PostAsync(
        "https://{{WIDGET_LOADER_ENDPOINT}}/Auth", 
        content
    );
    
    response.EnsureSuccessStatusCode();
    
    var token = await response.Content.ReadAsStringAsync();
    return token.Trim('"'); // Remove surrounding quotes if present
}
const axios = require('axios');

async function getAuthToken() {
  const response = await axios.post('https://{{WIDGET_LOADER_ENDPOINT}}/Auth', 
    new URLSearchParams({
      apikey: 'YOUR_API_KEY',
      secretKey: 'YOUR_SECRET_KEY'
    }),
    {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    }
  );
  
  return response.data.replace(/"/g, '');
}

Client-Side Implementation

// Token injected by your server (e.g., via template rendering)
const authToken = '@Model.AuthToken'; // ASP.NET MVC

const options = {
  destinationId: 'widget-container',
  authToken: authToken,  // Use authToken INSTEAD of apiKey
  userId: 'unique-user-identifier',
  width: '100%',
  height: '800px',
  theme: 'light'
};

const widget = optionsPlayWidgetLoader.load('pro', options);
⚠️ Important

The auth_token is one-time use. It's marked as expired immediately after the access_token is issued. Request a fresh token for each widget load.

Widget Configuration

Required Options

Option Type Description
destinationId string ID of the DOM element to embed the widget in
apiKey string Your API key (use this OR authToken, not both)
authToken string Server-generated token (use this OR apiKey, not both)
userId string Unique identifier for the end user

Display Options

Option Type Default Description
width string '100%' Widget width (CSS value)
height string '768px' Widget height (CSS value)
theme string 'light' Theme: 'light', 'dark', or 'three-panel-light'
symbol string Initial symbol to display

Feature Toggles

Option Type Default Description
showHeader boolean true Display the widget header
showMarketToneInHeader boolean true Show market tone indicator in header
showWhatPanel boolean true Display the "What" panel
showSupportButtons boolean false Show FAQ, Support, Feedback buttons
showDisclaimer boolean false Show Disclaimer button in footer
showBusinessInquiries boolean false Show Business Inquiries button
showSocialButtons boolean false Show social sharing buttons
showNewsTab boolean false Show News tab in Why panel
hideLocalizationOptions boolean false Hide language/locale options

Complete Example

const options = {
  // Required
  destinationId: 'options-play-container-full',
  apiKey: 'YOUR_API_KEY',
  userId: 'user001-prod',
  
  // Display
  width: '100%',
  height: '900px',
  theme: 'three-panel-light',
  symbol: 'MSFT',
  
  // Features
  showHeader: true,
  showMarketToneInHeader: true,
  showWhatPanel: true,
  hideLocalizationOptions: true,
  showSupportButtons: false,
  showDisclaimer: false,
  showBusinessInquiries: false,
  showSocialButtons: false,
  showNewsTab: false
};

const widget = optionsPlayWidgetLoader.load('pro', options);

User Identification

The userId parameter is required and should uniquely identify each end user. This enables:

  • User-specific analytics and reporting
  • Personalized experience persistence

Best practices:

  • Use a stable, unique identifier (UUID, hashed email, internal user ID)
  • Don't use PII directly (email addresses, names)
  • Keep it consistent across sessions for the same user
// Good
userId: 'usr_a1b2c3d4e5f6'
userId: 'acct-12345-trading'

// Avoid
userId: 'john@example.com'  // PII
userId: Math.random().toString() // Not stable

Events API

The widget communicates with your platform via HTML5 postMessage. This enables two-way, cross-domain communication.

Event Flow

sequenceDiagram participant Embedder as Embedder Page
(Parent Window) participant Widget as OptionsPlay Widget
(iframe) participant User Note over Embedder,Widget: Embedder → Widget User->>Embedder: Selects symbol "AAPL" Embedder->>Widget: postMessage(SET_SYMBOL, {symbol, sentiment}) Widget->>Widget: Updates display Widget-->>Embedder: SYMBOL_CHANGED event Note over Embedder,Widget: Widget → Embedder User->>Widget: Clicks "Trade" button Widget-->>Embedder: TRADE_TICKET_EXECUTE event Embedder->>Embedder: Routes to OMS / displays modal

Event Flow Between Embedder and Widget

Listening to Events

const widget = optionsPlayWidgetLoader.load('pro', options);

// Symbol changed (fired when user changes symbol in widget)
widget.on(widget.EventType.SYMBOL_CHANGED, function(newSymbol) {
  console.log('Symbol changed to:', newSymbol);
  document.getElementById('current-symbol').textContent = newSymbol;
});

// Trade ticket execution (fired when user clicks Trade button)
widget.on(widget.EventType.TRADE_TICKET_EXECUTE, function(tradeTicket) {
  console.log('Trade requested:', tradeTicket);
  
  // Route to your order management system
  sendToOMS({
    symbol: tradeTicket.symbol,
    strategy: tradeTicket.strategyName,
    legs: tradeTicket.optionLegs,
    price: tradeTicket.price
  });
});

Sending Commands to Widget

// Change the symbol displayed in the widget
widget.postMessage(widget.EventType.SET_SYMBOL, {
  symbol: 'AAPL',
  sentiment: 'Bullish'  // 'Bullish', 'Bearish', or 'Neutral'
});

Event Types Reference

Inbound Events (Widget → Embedder)

Event Payload Description
SYMBOL_CHANGED string Fired when the displayed symbol changes
TRADE_TICKET_EXECUTE TradeTicket Fired when user executes a trade

Outbound Events (Embedder → Widget)

Event Payload Description
SET_SYMBOL {symbol: string, sentiment?: string} Change the displayed symbol

TradeTicket Object

interface TradeTicket {
  symbol: string;           // e.g., "AAPL"
  strategyName: string;     // e.g., "Bull Call Spread"
  price: number;            // Total trade price
  optionLegs: OptionLeg[];  // Array of option legs
  securityLegs: SecurityLeg[]; // Array of underlying legs
}

interface OptionLeg {
  ask: number;      // Ask price of underlying
  bid: number;      // Bid price of underlying
  price: number;    // Price used for calculations
  quantity: number; // Positive = buy, negative = sell
  expiry: string;   // Expiration date
  strike: number;   // Strike price
}

interface SecurityLeg {
  ask: number;      // Ask price
  bid: number;      // Bid price
  price: number;    // Price used for calculations
  quantity: number; // Positive = buy, negative = sell shares
}

Security

SSL/TLS Required

All communication between your platform and OptionsPlay occurs over HTTPS. Your embedding page must be served over HTTPS to:

  • Enable CORS (Cross-Origin Resource Sharing)
  • Ensure iframe embedding works correctly
  • Protect user data in transit

Domain Whitelisting

When using API key authentication, requests are validated against your whitelisted domains. The widget will fail to load if:

  • The page isn't served from a whitelisted domain
  • The request comes from localhost (unless explicitly whitelisted for development)

To add domains: Contact your OptionsPlay administrator with:

  • Production domains (e.g., trading.yourcompany.com)
  • Staging/QA domains (e.g., staging.yourcompany.com)
  • Development domains if needed (e.g., localhost:3000)

Token Security

Token Type Exposure Validity Storage
apiKey Client-side (public) Permanent Code/config
secretKey Server-side only Permanent Secure vault
auth_token Client-side (ephemeral) One-time use Never persist
access_token Widget internal 1 day Managed by widget
session_cookie Widget internal Session Managed by widget
⚠️ Security Warning

Never expose your secretKey in client-side code. It should only exist on your server.

Content Security Policy (CSP)

If your application uses CSP headers, ensure you allow:

frame-src https://*.optionsplay.com;
script-src https://*.optionsplay.com;
connect-src https://*.optionsplay.com wss://*.optionsplay.com;

The wss:// directive is required for SignalR real-time data connections.

Framework Integration

import { useEffect, useRef } from 'react';

function OptionsPlayWidget({ apiKey, userId, symbol, onTradeExecute, onSymbolChange }) {
  const widgetRef = useRef(null);
  const containerRef = useRef(null);

  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://{{WIDGET_LOADER_ENDPOINT}}/widgetLoader';
    script.async = true;
    
    script.onload = () => {
      const options = {
        destinationId: containerRef.current.id,
        apiKey,
        userId,
        width: '100%',
        height: '100%',
        theme: 'light',
        symbol
      };

      widgetRef.current = window.optionsPlayWidgetLoader.load('pro', options);

      widgetRef.current.on(
        widgetRef.current.EventType.TRADE_TICKET_EXECUTE,
        onTradeExecute
      );
      
      widgetRef.current.on(
        widgetRef.current.EventType.SYMBOL_CHANGED,
        onSymbolChange
      );
    };

    document.body.appendChild(script);

    return () => {
      if (widgetRef.current) {
        widgetRef.current.destroyWidget();
      }
      document.body.removeChild(script);
    };
  }, [apiKey, userId]);

  useEffect(() => {
    if (widgetRef.current && symbol) {
      widgetRef.current.postMessage(
        widgetRef.current.EventType.SET_SYMBOL,
        { symbol, sentiment: 'Neutral' }
      );
    }
  }, [symbol]);

  return (
    <div 
      ref={containerRef}
      id="optionsplay-widget-container"
      style={{ width: '100%', height: '800px' }}
    />
  );
}

export default OptionsPlayWidget;
<template>
  <div ref="container" :id="containerId" :style="containerStyle" />
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';

const props = defineProps({
  apiKey: { type: String, required: true },
  userId: { type: String, required: true },
  symbol: { type: String, default: 'AAPL' },
  height: { type: String, default: '800px' }
});

const emit = defineEmits(['trade-execute', 'symbol-change']);

const containerId = 'optionsplay-widget-' + Math.random().toString(36).substr(2, 9);
const container = ref(null);
let widget = null;

const containerStyle = { width: '100%', height: props.height };

onMounted(() => {
  const script = document.createElement('script');
  script.src = 'https://{{WIDGET_LOADER_ENDPOINT}}/widgetLoader';
  script.async = true;
  
  script.onload = () => {
    widget = window.optionsPlayWidgetLoader.load('pro', {
      destinationId: containerId,
      apiKey: props.apiKey,
      userId: props.userId,
      width: '100%',
      height: '100%',
      theme: 'light',
      symbol: props.symbol
    });

    widget.on(widget.EventType.TRADE_TICKET_EXECUTE, (ticket) => {
      emit('trade-execute', ticket);
    });

    widget.on(widget.EventType.SYMBOL_CHANGED, (newSymbol) => {
      emit('symbol-change', newSymbol);
    });
  };

  document.body.appendChild(script);
});

onUnmounted(() => {
  if (widget) widget.destroyWidget();
});

watch(() => props.symbol, (newSymbol) => {
  if (widget && newSymbol) {
    widget.postMessage(widget.EventType.SET_SYMBOL, { 
      symbol: newSymbol, 
      sentiment: 'Neutral' 
    });
  }
});
</script>
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';

declare const optionsPlayWidgetLoader: any;

@Component({
  selector: 'app-optionsplay-widget',
  template: `<div #container [id]="containerId" [style.width]="'100%'" [style.height]="height"></div>`
})
export class OptionsPlayWidgetComponent implements OnInit, OnDestroy {
  @Input() apiKey!: string;
  @Input() userId!: string;
  @Input() symbol = 'AAPL';
  @Input() height = '800px';
  
  @Output() tradeExecute = new EventEmitter<any>();
  @Output() symbolChange = new EventEmitter<string>();

  @ViewChild('container', { static: true }) container!: ElementRef;

  containerId = 'optionsplay-widget-' + Math.random().toString(36).substr(2, 9);
  private widget: any;

  ngOnInit() {
    this.loadScript().then(() => this.initWidget());
  }

  ngOnDestroy() {
    if (this.widget) this.widget.destroyWidget();
  }

  private loadScript(): Promise<void> {
    return new Promise((resolve) => {
      if ((window as any).optionsPlayWidgetLoader) {
        resolve();
        return;
      }
      const script = document.createElement('script');
      script.src = 'https://{{WIDGET_LOADER_ENDPOINT}}/widgetLoader';
      script.onload = () => resolve();
      document.body.appendChild(script);
    });
  }

  private initWidget() {
    this.widget = optionsPlayWidgetLoader.load('pro', {
      destinationId: this.containerId,
      apiKey: this.apiKey,
      userId: this.userId,
      width: '100%',
      height: '100%',
      theme: 'light',
      symbol: this.symbol
    });

    this.widget.on(this.widget.EventType.TRADE_TICKET_EXECUTE, (ticket: any) => {
      this.tradeExecute.emit(ticket);
    });

    this.widget.on(this.widget.EventType.SYMBOL_CHANGED, (newSymbol: string) => {
      this.symbolChange.emit(newSymbol);
    });
  }

  setSymbol(symbol: string, sentiment = 'Neutral') {
    if (this.widget) {
      this.widget.postMessage(this.widget.EventType.SET_SYMBOL, { symbol, sentiment });
    }
  }
}

For Electron, WPF WebView, Qt WebView, or similar browser-less environments:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="x-ua-compatible" content="IE=Edge" />
  <title>OptionsPlay Widget</title>
</head>
<body style="margin: 0; padding: 0;">
  <div id="widget-container" style="width: 100vw; height: 100vh;"></div>
  
  <script src="https://{{WIDGET_LOADER_ENDPOINT}}/widgetLoader"></script>
  <script>
    // Token provided by your backend (one-time use)
    const authToken = '%%AUTH_TOKEN%%'; // Replace at runtime
    
    const widget = optionsPlayWidgetLoader.load('pro', {
      destinationId: 'widget-container',
      authToken: authToken,  // Use authToken, not apiKey
      userId: '%%USER_ID%%',
      width: '100%',
      height: '100%',
      theme: 'light'
    });
  </script>
</body>
</html>

Troubleshooting

Widget Won't Load

Symptom Likely Cause Solution
401 Unauthorized Invalid API key Verify your API key with OptionsPlay admin
403 Forbidden Domain not whitelisted Submit your domain for whitelisting
Widget container empty Invalid destinationId Ensure the DOM element exists before calling load()
Console: CORS error Page not served over HTTPS Serve your page via HTTPS
Console: CSP violation Content Security Policy blocking Add OptionsPlay domains to CSP headers

Events Not Firing

Symptom Likely Cause Solution
SYMBOL_CHANGED not received Event listener added after widget load Add listeners immediately after load() returns
TRADE_TICKET_EXECUTE not received Trade button not visible Check widget configuration (showHeader, etc.)
postMessage has no effect Wrong event type string Use widget.EventType.SET_SYMBOL, not a string

Performance Issues

Symptom Likely Cause Solution
Slow initial load Large page with many resources Load widget script async
Sluggish interactions Too many widgets on one page Limit to 1-2 widgets per page
Memory leaks Widget not destroyed on navigation Call widget.destroyWidget() on unmount

Development Tips

  • Use browser DevTools Network tab to inspect requests to *.optionsplay.com
  • Check console for errors — the widget logs issues to console
  • Test in incognito to rule out extension conflicts
  • Verify HTTPS — even in development, use https://localhost if possible

API Reference

optionsPlayWidgetLoader.load(widgetType, options, callback?)

Loads and embeds an OptionsPlay widget.

Parameter Type Description
widgetType 'pro' | 'proStrategies' Type of widget to load
options WidgetOptions Configuration object
callback (widget) => void Optional callback when widget iframe is ready

Returns: WidgetInstance

WidgetInstance.on(eventType, callback)

Subscribe to widget events.

Parameter Type Description
eventType EventType Event to listen for
callback (payload) => void Handler function

WidgetInstance.postMessage(eventType, payload)

Send a command to the widget.

Parameter Type Description
eventType EventType Command type
payload object Command data

WidgetInstance.destroyWidget()

Destroys the widget, cleans up the iframe, and terminates the server session.

Always call this when:

  • User navigates away from the page
  • Widget is removed from DOM
  • Component unmounts (React/Vue/Angular)

WidgetInstance.EventType

Available event type constants:

widget.EventType.SYMBOL_CHANGED      // Inbound: symbol changed in widget
widget.EventType.TRADE_TICKET_EXECUTE // Inbound: user executed trade
widget.EventType.SET_SYMBOL          // Outbound: change displayed symbol