| name | jsapi-filters |
| description | Apply runtime filters to embedded Domo dashboards/cards from the host page via the JS API (MessagePort). Covers dynamic filtering, drill events, filter change listeners, iframe resize, pfilter URL params, overrideFilters, and App Studio appData. Use for any client-side Domo embed interaction. Not for server-side programmatic filters (use programmatic-filters). |
Domo Client-Side Filters (JS API)
Dynamically filter embedded Domo content from your host page after the embed loads. Unlike server-side programmatic filters (baked into the embed token), client-side filters update instantly without re-fetching tokens.
Use for: UI-driven filtering (dropdowns, date pickers), drill-down responses, syncing filters across multiple embeds, passing context to App Studio apps. For server-side filters, see programmatic-filters.
How It Works
Domo iframes communicate with the host page via MessagePort (JSON-RPC 2.0):
- Domo iframe posts a
message event with a MessagePort and referenceId
- Host page captures the port and listens for messages
- Host sends RPC calls (filters) through the port; Domo sends events (drills, filter changes) back
Setup
Step 1: Initialize the JS API Listener
Set up a global listener to capture each iframe's MessagePort handshake.
const ports: Record<string, MessagePort> = {};
export interface DomoRpcMessage extends MessageEvent {
data: {
referenceId?: string;
method?: string;
params?: any;
result?: any;
error?: any;
id?: string;
jsonrpc?: string;
};
}
export const initializeJsApi = () => {
if (typeof window === 'undefined') return;
const handleMessage = (e: MessageEvent) => {
if (!e.ports || !e.ports[0]) return;
const referenceId = e.data?.referenceId;
if (!referenceId) return;
const port = e.ports[0];
port.start();
ports[referenceId] = port;
port.onmessage = (event: MessageEvent) => {
const data = event.data;
if (!data) return;
if (data.method) {
handleRpcMethod(referenceId, data.method, data.params);
}
if (data.error) {
console.error(`[JsApi] RPC error (ref: ${referenceId}):`, data.error);
}
};
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
};
Step 2: Mount the Initializer Early
Mount before any Domo iframe loads (app root). React/Next.js:
'use client'
import { useEffect } from 'react'
import { initializeJsApi } from '@/lib/jsapi'
export default function DomoJsApiInitializer() {
useEffect(() => {
const cleanup = initializeJsApi()
return cleanup
}, [])
return null
}
Then include it in your root layout:
import DomoJsApiInitializer from './components/DomoJsApiInitializer'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<DomoJsApiInitializer />
{children}
</body>
</html>
)
}
Step 3: Configure Embed Authorized Domains
Add your host domain to Admin > Network Security > Embed Authorized Domains in Domo. Without this, the iframe won't send the handshake message event. For custom Domo apps, also add to Custom Apps Authorized Domains.
Step 4: Use Embed Tokens (Not Public URLs)
The JS API only works with authenticated embeds (token-based POST flow). Public embed URLs don't initiate the MessagePort handshake. See programmatic-filters for the token flow.
Applying Filters
Send /v1/filters/apply through the stored MessagePort.
Filter Structure
Client-side filters use operand (not operator like server-side filters — this is the most common bug):
interface ClientFilter {
column: string;
operand: string;
values: any[];
dataType?: string;
dataSourceId?: string;
}
applyFilters Function
export const applyFilters = (
filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
Object.values(ports).forEach(port => {
port.postMessage({
id: `filter-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/filters/apply',
params: {
filters
}
});
});
};
This broadcasts the filters to all connected Domo iframes. To target a specific iframe, use the referenceId:
export const applyFiltersToEmbed = (
referenceId: string,
filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
const port = ports[referenceId];
if (!port) {
console.warn(`[JsApi] No port found for referenceId: ${referenceId}`);
return;
}
port.postMessage({
id: `filter-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/filters/apply',
params: { filters }
});
};
Examples
Filter by a single value:
applyFilters([
{ column: 'Region', operand: 'IN', values: ['West'] }
]);
Multiple filters (ANDed together):
applyFilters([
{ column: 'Region', operand: 'IN', values: ['West', 'East'] },
{ column: 'Year', operand: 'EQUALS', values: [2025] }
]);
Clear all filters:
applyFilters([]);
React Example — Filter Dropdown
'use client'
import { useState } from 'react'
import { applyFilters } from '@/lib/jsapi'
const regions = ['All', 'West', 'East', 'North', 'South']
export default function RegionFilter() {
const [selected, setSelected] = useState('All')
const handleChange = (region: string) => {
setSelected(region)
if (region === 'All') {
applyFilters([])
} else {
applyFilters([
{ column: 'Region', operand: 'IN', values: [region] }
])
}
}
return (
<select value={selected} onChange={(e) => handleChange(e.target.value)}>
{regions.map(r => (
<option key={r} value={r}>{r}</option>
))}
</select>
)
}
Handling Events from Domo
Domo sends events back through the MessagePort. Use a subscribe/unsubscribe pattern for clean listener management:
type EventCallback = (referenceId: string, params: any) => void;
const listeners: Record<string, Set<EventCallback>> = {};
export function on(event: string, callback: EventCallback): () => void {
if (!listeners[event]) listeners[event] = new Set();
listeners[event].add(callback);
return () => { listeners[event].delete(callback); };
}
Then in your handleRpcMethod, dispatch to registered listeners. Handle them in a central switch:
function handleRpcMethod(referenceId: string, method: string, params: any) {
switch (method) {
case '/v1/onDrill':
handleDrill(referenceId, params);
break;
case '/v1/onFiltersChange':
handleFiltersChange(referenceId, params);
break;
case '/v1/onAppReady':
handleAppReady(referenceId, params);
break;
case '/v1/onAppData':
handleAppData(referenceId, params);
break;
case '/v1/onFrameSizeChange':
handleFrameResize(referenceId, params);
break;
default:
break;
}
}
/v1/onDrill — Drill Events
Fired when a user clicks a drillable element. Includes filters representing the drill path:
function handleDrill(referenceId: string, params: any) {
const drillFilters = params?.filters;
applyFiltersToEmbed('other-embed-ref', drillFilters);
router.push(`/details?region=${drillFilters[0]?.values[0]}`);
}
/v1/onFiltersChange — Filter Change Events
Fired when filters change inside the embed (user interaction, card-to-card, or programmatic). Use to sync host UI:
function handleFiltersChange(referenceId: string, params: any) {
const currentFilters = params?.filters;
setActiveFilters(currentFilters);
applyFiltersToEmbed('second-dashboard-ref', currentFilters);
}
/v1/onAppReady — App Studio Ready
Fired when an App Studio app finishes loading. Critical with overrideFilters=true — send filters only after this event:
function handleAppReady(referenceId: string, params: any) {
applyFiltersToEmbed(referenceId, initialFilters);
sendAppData(referenceId, { customerId: '12345' });
}
/v1/onAppData — App Data from Domo
Fired when an App Studio app sends data back to the host:
function handleAppData(referenceId: string, params: any) {
const appData = params?.appData;
console.log('App data received:', appData);
}
/v1/onFrameSizeChange — Frame Resize
Fired when embed content resizes. Use to adjust iframe container:
function handleFrameResize(referenceId: string, params: any) {
const { width, height } = params || {};
const iframe = document.querySelector(`iframe[name="${referenceId}"]`);
if (iframe && height) {
(iframe as HTMLIFrameElement).style.height = `${height}px`;
}
}
Sending Data to App Studio Apps
Pass arbitrary context to App Studio apps via /v1/appData/apply:
export const sendAppData = (referenceId: string, appData: any) => {
const port = ports[referenceId];
if (!port) return;
port.postMessage({
id: `appData-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/appData/apply',
params: { appData }
});
};
The embedded app receives this via domo.onAppData(). Use for passing user IDs, customer context, or preferences.
PFilter URL Parameters
A no-JS alternative: append filters as URL query params. Trade-off: triggers full iframe refresh and filters are visible/modifiable in the URL.
Structure
?pfilters=[{"column":"ColumnName","operand":"OPERATOR","values":["val1","val2"]}]
Example
https://example.domo.com/embed/pages/private/ABCDE?pfilters=[{"column":"Region","operand":"IN","values":["West","East"]},{"column":"Year","operand":"EQUALS","values":["2025"],"dataType":"NUMBER"}]
Optional Fields
dataSourceId — target a specific dataset
dataType — STRING, NUMBER, DATE
When to Use PFilters vs JS API
| PFilters | JS API |
|---|
| Setup required | None | MessagePort initialization |
| Speed | Slower (iframe refresh) | Instant (no reload) |
| Security | Visible in URL, user-modifiable | Visible in DOM, user-modifiable |
| Multiple updates | Each change refreshes iframe | Updates in place |
| Best for | Simple one-time filters, link sharing | Interactive filtering, dynamic UIs |
Security warning: PFilters are visible in the URL and can be modified by end users. They provide no security — use server-side programmatic filters for data isolation.
overrideFilters for App Studio
The overrideFilters=true URL parameter tells App Studio apps to ignore saved defaults and wait for host-provided filters:
https://example.domo.com/embed/pages/private/ABCDE?overrideFilters=true
Behavior:
overrideFilters=true — app loads with no filters, waits for host to send them
overrideFilters=false or omitted — app loads its saved defaults normally
Important: You must send at least one /v1/filters/apply (even empty []) or the app stays in a waiting state. Listen for /v1/onAppReady first.
function handleAppReady(referenceId: string, params: any) {
applyFiltersToEmbed(referenceId, userFilters.length > 0 ? userFilters : []);
}
Other useful URL parameters for embedded content:
| Parameter | Purpose |
|---|
transparentBackground=true | Makes dashboard background transparent |
appData=value | Pass initial context to App Studio apps (spaces as +) |
viewId=abc123 | Specify which App Studio page view to display |
Known RPC Methods
Host → Domo (Commands)
| Method | Description |
|---|
/v1/filters/apply | Apply or clear runtime filters |
/v1/appData/apply | Send context data to App Studio apps |
Domo → Host (Events)
| Method | Description |
|---|
/v1/onDrill | User drilled into a data point |
/v1/onFiltersChange | Filters changed inside the embed |
/v1/onAppReady | App Studio app finished loading, ready for input |
/v1/onAppData | App Studio app sent data back to host |
/v1/onFrameSizeChange | Embedded content changed dimensions |
Combining with Server-Side Filters
- Server-side: Security boundary. Enforced by Domo, user cannot bypass. Use for data isolation.
- Client-side: UX convenience. User can change. Use for interactive filtering.
Client-side filters narrow within server-side permitted data — they cannot override server-side restrictions.
Gotchas and Best Practices
Port Availability Timing
Calling applyFilters before the handshake completes silently fails. Two approaches:
Dashboards/cards: Queue and flush on connect:
const pendingFilters: Record<string, Array<{ column: string; operand: string; values: any[] }>> = {};
ports[referenceId] = port;
if (pendingFilters[referenceId]) {
applyFiltersToEmbed(referenceId, pendingFilters[referenceId]);
delete pendingFilters[referenceId];
}
App Studio apps: Listen for /v1/onAppReady first, then send filters. Required with overrideFilters=true.
operand vs operator
Client-side uses operand, server-side uses operator. Mixing them up causes silent failures.
referenceId Mapping
The referenceId is assigned by the Domo iframe — you don't control it. For multiple embeds, correlate handshake timing with render order. For a single embed, use the broadcast applyFilters().
Operator Typo in Domo API
GREATER_THAN_EQUALS_TO sometimes appears as GREAT_THAN_EQUALS_TO (missing ER). Try both spellings if comparison operators fail.
Multiple Iframes
Each iframe gets its own port. applyFilters broadcasts to all; use applyFiltersToEmbed with a specific referenceId for independent filtering.
Public Embeds Don't Support JS API
Only authenticated (token-based) embeds initiate the MessagePort handshake. Public embed URLs don't work.
Supported Operands
IN, NOT_IN, EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_EQUALS_TO, LESS_THAN, LESS_THAN_EQUALS_TO, BETWEEN (exactly two values), CONTAINS (partial string match).
No SQL Filters Client-Side
/v1/filters/apply only supports standard filter objects. SQL filters are server-side only.
Security
Client-side filters and pfilters are for UX only — visible and modifiable by end users. Use server-side programmatic filters for data isolation.
TypeScript Type Definitions
type ClientFilterOperand =
| 'IN'
| 'NOT_IN'
| 'EQUALS'
| 'NOT_EQUALS'
| 'GREATER_THAN'
| 'GREATER_THAN_EQUALS_TO'
| 'LESS_THAN'
| 'LESS_THAN_EQUALS_TO'
| 'BETWEEN'
| 'CONTAINS';
interface ClientFilter {
column: string;
operand: ClientFilterOperand;
values: (string | number | boolean)[];
dataType?: 'STRING' | 'NUMBER' | 'DATE';
dataSourceId?: string;
}
interface DomoRpcRequest {
id: string;
jsonrpc: '2.0';
method: string;
params: Record<string, any>;
}
interface DomoRpcEvent {
method: string;
params?: Record<string, any>;
result?: any;
error?: { code: number; message: string };
}
interface DrillEventParams {
filters: ClientFilter[];
}
interface FiltersChangeParams {
filters: ClientFilter[];
}
interface FrameSizeChangeParams {
width: number;
height: number;
}
interface AppDataParams {
appData: any;
}
Quick Reference
Read references/rpc-methods.md for the full RPC method reference and message format details.