window.openai API Complete Reference Guide

The window.openai API is the JavaScript interface that powers all interactive ChatGPT widgets. Whether you're building inline cards, fullscreen experiences, or picture-in-picture components, this API provides the methods you need to create responsive, state-aware widgets that feel native to the ChatGPT experience.

This guide is part of our Complete Guide to Building ChatGPT Applications.

This comprehensive reference covers every method, parameter, and best practice for working with the window.openai API.

API Overview

What is window.openai?

The window.openai object is automatically injected into every ChatGPT widget runtime environment. It provides a JavaScript bridge between your widget code and the ChatGPT platform, enabling:

  • Persistent state management across widget instances
  • Navigation and view transitions for multi-screen experiences
  • User interaction methods (toasts, confirmations, actions)
  • Event handling for platform lifecycle events
  • Tool invocation to call your MCP server functions

Unlike traditional web APIs, window.openai is specifically designed for conversational AI contexts. Methods are optimized for the unique challenges of widgets that appear inline with chat messages, update based on conversation context, and maintain state across ChatGPT sessions.

Browser Compatibility

The window.openai API is available in:

  • ChatGPT Web (desktop browsers: Chrome 90+, Safari 14+, Firefox 88+, Edge 90+)
  • ChatGPT iOS App (iOS 15.0+)
  • ChatGPT Android App (Android 8.0+)

Key differences across platforms:

Feature Web iOS Android
State Management ✅ Full support ✅ Full support ✅ Full support
Navigation (showView/hideView) ✅ Full support ✅ Full support ✅ Full support
Toast Notifications ✅ Full support ⚠️ Native iOS alerts ⚠️ Native Android toasts
Confirm Dialogs ✅ Full support ✅ Native dialogs ✅ Native dialogs
Custom Fonts ❌ System fonts only ❌ System fonts only ❌ System fonts only

TypeScript Support

The window.openai API includes TypeScript definitions for type-safe development:

declare global {
  interface Window {
    openai: {
      setWidgetState(state: Record<string, any>): Promise<void>;
      getWidgetState(): Promise<Record<string, any>>;
      showView(viewId: string, props?: Record<string, any>): void;
      hideView(): void;
      goBack(): void;
      showToast(message: string, options?: ToastOptions): void;
      confirmAction(message: string, options?: ConfirmOptions): Promise<boolean>;
      invokeTool(toolName: string, params: Record<string, any>): Promise<any>;
      addEventListener(event: string, handler: Function): void;
      removeEventListener(event: string, handler: Function): void;
    };
  }
}

Initialization Check

Always verify window.openai is available before calling methods:

if (typeof window.openai === 'undefined') {
  console.error('window.openai API not available. Widget running outside ChatGPT runtime.');
  // Fallback to local state management or mock API
}

For production widgets, provide graceful degradation when the API is unavailable (e.g., during local development with MCP Inspector).


State Management Methods

State management is the foundation of interactive ChatGPT widgets. The window.openai API provides three methods for managing widget state that persists across chat sessions and widget re-renders.

setWidgetState(state)

Signature:

setWidgetState(state: Record<string, any>): Promise<void>

Sets the widget's persistent state. This state is stored by the ChatGPT platform and survives:

  • Widget re-renders (when the user scrolls away and back)
  • ChatGPT app restarts
  • Cross-device syncing (if user is logged in)

Parameters:

  • state (Object): A plain JavaScript object containing the state to persist. Must be JSON-serializable (no functions, Date objects, or circular references).

Returns: Promise that resolves when state is persisted.

Example: Todo List Widget

// Add a new todo item
async function addTodo(text) {
  const currentState = await window.openai.getWidgetState();
  const todos = currentState.todos || [];

  const newTodo = {
    id: Date.now(),
    text: text,
    completed: false
  };

  await window.openai.setWidgetState({
    ...currentState,
    todos: [...todos, newTodo]
  });

  // Re-render UI with updated todos
  renderTodoList();
}

Best Practices:

  1. Keep state small (under 10KB recommended). Large state objects slow down widget load times.
  2. Avoid nested objects deeper than 3 levels. Flat structures are faster to serialize.
  3. Debounce frequent updates to avoid overwhelming the platform with state writes.
// Debounced state saver for text input
let stateDebounceTimer;
function saveFormState(formData) {
  clearTimeout(stateDebounceTimer);
  stateDebounceTimer = setTimeout(async () => {
    await window.openai.setWidgetState({ formData });
  }, 500); // Wait 500ms after last keystroke
}
  1. Never store sensitive data (tokens, passwords, API keys) in widget state. State is visible to the ChatGPT platform and potentially logged.

getWidgetState()

Signature:

getWidgetState(): Promise<Record<string, any>>

Retrieves the current widget state. Returns an empty object {} if no state has been set.

Returns: Promise that resolves to the current state object.

Example: Initialize Widget from Saved State

async function initializeWidget() {
  const state = await window.openai.getWidgetState();

  if (state.todos && state.todos.length > 0) {
    // Restore previous todo list
    renderTodoList(state.todos);
  } else {
    // First time user - show empty state
    renderEmptyState();
  }

  if (state.preferences) {
    // Restore user preferences (theme, sort order, etc.)
    applyPreferences(state.preferences);
  }
}

// Call on widget load
initializeWidget();

Best Practices:

  1. Always await the result before rendering UI. Synchronous rendering with stale data causes flickering.
  2. Provide defaults for missing state keys to avoid undefined errors.
const state = await window.openai.getWidgetState();
const todos = state.todos || [];
const theme = state.theme || 'light';
const sortOrder = state.sortOrder || 'date-desc';
  1. Cache state locally if accessed frequently within a single render cycle.
let stateCache = null;

async function getCachedState() {
  if (!stateCache) {
    stateCache = await window.openai.getWidgetState();
  }
  return stateCache;
}

// Invalidate cache when state changes
async function updateState(newState) {
  await window.openai.setWidgetState(newState);
  stateCache = newState; // Update cache
}

useWidgetState Hook (React)

Signature:

function useWidgetState<T>(initialState: T): [T, (newState: T) => Promise<void>]

React hook that provides a useState-like interface for widget state. Automatically handles persistence via window.openai.setWidgetState.

Parameters:

  • initialState (any): Default state value used if no persisted state exists.

Returns: Array with [state, setState] (similar to React's useState).

Example: Counter Widget with React

import { useWidgetState } from '@openai/widget-runtime';

function CounterWidget() {
  const [count, setCount] = useWidgetState(0);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  );
}

Example: Form with Multiple State Fields

function ContactFormWidget() {
  const [formData, setFormData] = useWidgetState({
    name: '',
    email: '',
    message: ''
  });

  const updateField = (field, value) => {
    setFormData({ ...formData, [field]: value });
  };

  return (
    <form>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => updateField('name', e.target.value)}
        placeholder="Your name"
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => updateField('email', e.target.value)}
        placeholder="Your email"
      />
      <textarea
        value={formData.message}
        onChange={(e) => updateField('message', e.target.value)}
        placeholder="Message"
      />
      <button type="submit">Send</button>
    </form>
  );
}

Best Practices:

  1. Use separate state hooks for unrelated data to avoid unnecessary re-renders.
// Good: Separate state for independent data
const [todos, setTodos] = useWidgetState([]);
const [preferences, setPreferences] = useWidgetState({ theme: 'light' });

// Bad: Single state object causes full re-render on any change
const [state, setState] = useWidgetState({ todos: [], preferences: {} });
  1. Memoize expensive computations derived from widget state.
const [todos, setTodos] = useWidgetState([]);

const completedCount = useMemo(() => {
  return todos.filter(t => t.completed).length;
}, [todos]);

Navigation Methods

Navigation methods control multi-screen experiences within widgets. Use these for fullscreen apps with multiple views (settings pages, detail screens, wizards, etc.).

showView(viewId, props)

Signature:

showView(viewId: string, props?: Record<string, any>): void

Displays a new view, pushing it onto the navigation stack. The current view remains in the stack and can be returned to via goBack().

Parameters:

  • viewId (string): Unique identifier for the view (e.g., "settings", "item-detail").
  • props (Object, optional): Data to pass to the new view.

Example: Multi-Page Recipe App

// Main recipe list view
function renderRecipeList(recipes) {
  const container = document.getElementById('app');
  container.innerHTML = recipes.map(recipe => `
    <div class="recipe-card" onclick="viewRecipeDetail('${recipe.id}')">
      <h3>${recipe.name}</h3>
      <p>${recipe.description}</p>
    </div>
  `).join('');
}

// Navigate to recipe detail view
function viewRecipeDetail(recipeId) {
  const recipe = getRecipeById(recipeId);
  window.openai.showView('recipe-detail', { recipe });
}

// Listen for view change
window.openai.addEventListener('viewDidAppear', (event) => {
  if (event.viewId === 'recipe-detail') {
    renderRecipeDetail(event.props.recipe);
  }
});

function renderRecipeDetail(recipe) {
  const container = document.getElementById('app');
  container.innerHTML = `
    <button onclick="window.openai.goBack()">← Back to Recipes</button>
    <h1>${recipe.name}</h1>
    <img src="${recipe.image}" alt="${recipe.name}">
    <h2>Ingredients</h2>
    <ul>${recipe.ingredients.map(i => `<li>${i}</li>`).join('')}</ul>
    <h2>Instructions</h2>
    <ol>${recipe.steps.map(s => `<li>${s}</li>`).join('')}</ol>
  `;
}

Best Practices:

  1. Limit navigation depth to 3-4 levels. Deep navigation hierarchies feel unnatural in conversational contexts.
  2. Always provide a back button or gesture. Users expect to navigate backwards.
  3. Preserve scroll position when returning to previous views.
const viewScrollPositions = {};

window.openai.addEventListener('viewWillDisappear', (event) => {
  viewScrollPositions[event.viewId] = window.scrollY;
});

window.openai.addEventListener('viewDidAppear', (event) => {
  if (viewScrollPositions[event.viewId]) {
    window.scrollTo(0, viewScrollPositions[event.viewId]);
  }
});

hideView()

Signature:

hideView(): void

Closes the current view and returns to the previous view in the navigation stack. Equivalent to pressing a "back" button.

Example: Modal Dialog

function showSettingsModal() {
  window.openai.showView('settings-modal');
}

function closeSettingsModal() {
  window.openai.hideView();
}

// Settings modal view
window.openai.addEventListener('viewDidAppear', (event) => {
  if (event.viewId === 'settings-modal') {
    renderSettingsModal();
  }
});

function renderSettingsModal() {
  const container = document.getElementById('app');
  container.innerHTML = `
    <div class="modal">
      <h2>Settings</h2>
      <label>
        <input type="checkbox" id="darkMode"> Dark Mode
      </label>
      <button onclick="saveSettings()">Save</button>
      <button onclick="window.openai.hideView()">Cancel</button>
    </div>
  `;
}

goBack()

Signature:

goBack(): void

Navigates to the previous view in the navigation stack. Identical to hideView() but more semantically clear when used with explicit back buttons.

Example: Wizard with Steps

let currentStep = 1;

function renderWizard() {
  const container = document.getElementById('app');
  container.innerHTML = `
    <div class="wizard">
      <h2>Step ${currentStep} of 3</h2>
      ${renderWizardStep(currentStep)}
      <div class="wizard-nav">
        ${currentStep > 1 ? '<button onclick="previousStep()">← Previous</button>' : ''}
        ${currentStep < 3 ? '<button onclick="nextStep()">Next →</button>' : '<button onclick="submitWizard()">Submit</button>'}
      </div>
    </div>
  `;
}

function nextStep() {
  if (currentStep < 3) {
    currentStep++;
    window.openai.showView(`wizard-step-${currentStep}`);
  }
}

function previousStep() {
  if (currentStep > 1) {
    currentStep--;
    window.openai.goBack();
  }
}

User Interaction Methods

User interaction methods provide native-feeling feedback and confirmation dialogs that match ChatGPT's design language.

showToast(message, options)

Signature:

showToast(message: string, options?: {
  type?: 'success' | 'error' | 'info' | 'warning';
  duration?: number;
}): void

Displays a temporary notification toast message.

Parameters:

  • message (string): Toast message text.
  • options.type (string): Toast variant (default: 'info').
  • options.duration (number): Display duration in milliseconds (default: 3000).

Example: Form Submission Feedback

async function submitContactForm(formData) {
  try {
    const result = await window.openai.invokeTool('submit_contact_form', formData);

    window.openai.showToast('Message sent successfully!', {
      type: 'success',
      duration: 4000
    });

    // Clear form
    document.getElementById('contact-form').reset();
  } catch (error) {
    window.openai.showToast('Failed to send message. Please try again.', {
      type: 'error',
      duration: 5000
    });
  }
}

Best Practices:

  1. Keep messages concise (under 50 characters). Long messages may be truncated on mobile.
  2. Use appropriate types for user expectations:
    • success: Green checkmark, action completed successfully
    • error: Red X, action failed
    • warning: Yellow exclamation, caution or non-blocking issue
    • info: Blue info icon, neutral information
  3. Adjust duration based on message importance (errors: 5s+, success: 3s).

confirmAction(message, options)

Signature:

confirmAction(message: string, options?: {
  title?: string;
  confirmText?: string;
  cancelText?: string;
  destructive?: boolean;
}): Promise<boolean>

Shows a confirmation dialog and returns user's choice.

Parameters:

  • message (string): Confirmation message.
  • options.title (string): Dialog title (default: "Confirm").
  • options.confirmText (string): Confirm button text (default: "OK").
  • options.cancelText (string): Cancel button text (default: "Cancel").
  • options.destructive (boolean): If true, confirm button is red (default: false).

Returns: Promise that resolves to true if user confirmed, false if canceled.

Example: Delete Confirmation

async function deleteTodo(todoId) {
  const confirmed = await window.openai.confirmAction(
    'Are you sure you want to delete this todo? This action cannot be undone.',
    {
      title: 'Delete Todo',
      confirmText: 'Delete',
      cancelText: 'Cancel',
      destructive: true
    }
  );

  if (confirmed) {
    const state = await window.openai.getWidgetState();
    const todos = state.todos.filter(t => t.id !== todoId);
    await window.openai.setWidgetState({ ...state, todos });

    window.openai.showToast('Todo deleted', { type: 'success' });
    renderTodoList();
  }
}

Example: Unsaved Changes Warning

let formDirty = false;

window.openai.addEventListener('viewWillDisappear', async (event) => {
  if (formDirty) {
    const discard = await window.openai.confirmAction(
      'You have unsaved changes. Are you sure you want to leave?',
      {
        title: 'Unsaved Changes',
        confirmText: 'Discard Changes',
        cancelText: 'Keep Editing',
        destructive: true
      }
    );

    if (!discard) {
      event.preventDefault(); // Cancel navigation
    }
  }
});

Event Handling

The window.openai API emits lifecycle events that widgets can listen to for initialization, cleanup, and state synchronization.

addEventListener(event, handler)

Signature:

addEventListener(event: string, handler: (event: any) => void): void

Registers an event listener for widget lifecycle events.

Available Events:

Event Fired When Use Case
widgetDidMount Widget first loads Initialize state, fetch data
widgetWillUnmount Widget about to be removed Cleanup timers, save state
viewDidAppear New view becomes visible Render view-specific UI
viewWillDisappear View about to be hidden Save scroll position, cleanup
stateDidChange Widget state updated Sync UI with new state
conversationDidUpdate New chat message received Update widget based on conversation

Example: Auto-Save on Unmount

let autoSaveTimer;

window.openai.addEventListener('widgetDidMount', async () => {
  console.log('Widget mounted, initializing...');
  await initializeWidget();

  // Start auto-save timer (every 30 seconds)
  autoSaveTimer = setInterval(async () => {
    const formData = getFormData();
    await window.openai.setWidgetState({ formData });
  }, 30000);
});

window.openai.addEventListener('widgetWillUnmount', async () => {
  console.log('Widget unmounting, saving state...');

  // Clear auto-save timer
  clearInterval(autoSaveTimer);

  // Final state save
  const formData = getFormData();
  await window.openai.setWidgetState({ formData });
});

Example: Sync Widget with Conversation Context

window.openai.addEventListener('conversationDidUpdate', (event) => {
  const lastMessage = event.messages[event.messages.length - 1];

  if (lastMessage.role === 'user' && lastMessage.content.includes('show me')) {
    // User asked to see something - highlight relevant items
    highlightSearchResults(lastMessage.content);
  }
});

removeEventListener(event, handler)

Signature:

removeEventListener(event: string, handler: Function): void

Unregisters a previously registered event listener. Always clean up listeners to prevent memory leaks.

Example: Temporary Event Listener

function setupOneTimeListener() {
  const handler = (event) => {
    console.log('State changed:', event.newState);
    // Remove listener after first trigger
    window.openai.removeEventListener('stateDidChange', handler);
  };

  window.openai.addEventListener('stateDidChange', handler);
}

Best Practices:

  1. Always remove listeners on unmount to prevent memory leaks.
const handlers = [];

function registerHandler(event, handler) {
  window.openai.addEventListener(event, handler);
  handlers.push({ event, handler });
}

window.openai.addEventListener('widgetWillUnmount', () => {
  handlers.forEach(({ event, handler }) => {
    window.openai.removeEventListener(event, handler);
  });
});
  1. Use named functions instead of anonymous functions for easier cleanup.
// Good: Named function can be removed
function handleStateChange(event) {
  console.log('State changed:', event);
}
window.openai.addEventListener('stateDidChange', handleStateChange);
window.openai.removeEventListener('stateDidChange', handleStateChange);

// Bad: Anonymous function cannot be removed
window.openai.addEventListener('stateDidChange', (event) => {
  console.log('State changed:', event);
});

Interactive Demo Widgets

See the window.openai API in action with these live demos:

  1. Todo List Widget - State management with setWidgetState/getWidgetState
  2. Recipe Browser - Multi-view navigation with showView/goBack
  3. Contact Form - Toast notifications and confirmation dialogs
  4. Shopping Cart - Event handling and conversation sync

All demo source code available on GitHub.


Related Articles


Build ChatGPT Widgets Without Code

The window.openai API provides powerful primitives for building interactive ChatGPT widgets, but implementing MCP servers, widget runtimes, and state management from scratch is time-consuming.

MakeAIHQ generates production-ready ChatGPT apps with window.openai integration built-in:

✅ Auto-generated MCP servers with proper tool annotations ✅ React widgets with useWidgetState hook pre-configured ✅ State management patterns following OpenAI best practices ✅ Browser compatibility handling for iOS/Android ✅ One-click deployment to ChatGPT App Store

Start building your ChatGPT app in 5 minutes →

Try our Instant App Wizard - answer 5 questions and get a fully functional ChatGPT app using the window.openai API.