cap-de-jsapi-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 cap-de-programmatic-filters.
How It Works
Domo iframes communicate with the host page via MessagePort (JSON-RPC 2.0):
- Domo iframe posts a
messageevent with aMessagePortandreferenceId - 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.
// lib/jsapi.ts
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();
// Store the port for sending commands later
ports[referenceId] = port;
// Listen for events from this iframe
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:
// components/DomoJsApiInitializer.tsx
'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:
// app/layout.tsx
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 cap-de-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; // 'IN', 'NOT_IN', 'EQUALS', 'BETWEEN', 'CONTAINS', etc.
values: any[];
dataType?: string; // 'STRING' | 'NUMBER' | 'DATE' — disambiguates values
dataSourceId?: string; // target a specific dataset in multi-dataset embeds
}
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:
// Event subscription pattern
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;
// drillFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]
// Example: apply the drill filters to another embed on the page
applyFiltersToEmbed('other-embed-ref', drillFilters);
// Example: navigate to a detail page
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;
// currentFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]
// Example: update your app's filter UI to reflect what's selected in Domo
setActiveFilters(currentFilters);
// Example: sync filters to another embed
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) {
// Now safe to send filters and appData
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;
// Handle data sent from the App Studio app
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 datasetdataType—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 themoverrideFilters=falseor 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) {
// App is ready — send the initial filter state
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[] }>> = {};
// When storing the port after handshake, flush pending filters
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.