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 |
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
(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();
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
(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);
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
(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 |
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://localhostif 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