Embedding with iframe
Complete reference for embedding Qyra content using iframes with JWTs in URL hash fragments
Embedding is available to all Qyra Cloud users and Enterprise On-Prem customers. Get in touch to have this feature enabled in your account.
Try it and see the code. embed.qyraflow.com is an interactive demo and helper that lets you preview embedded content and generate a JWT for your own project. For a working end-to-end example, see the example embed app on GitHub — a Node.js app that mints tokens server-side and renders a Qyra dashboard.
Overview
iframe embedding is the simplest way to embed Qyra dashboards and standalone data apps in your application. It requires no special libraries, dependencies, or CORS configuration—just generate a JWT and construct an embed URL.
iframe embedding supports dashboards and data apps. Chart embedding requires the React SDK.
Benefits of iframe embedding
- Simple integration - Standard HTML iframe element, works anywhere
- No dependencies - No JavaScript libraries or SDK installation required
- No CORS configuration - Unlike the React SDK, iframes don't require CORS setup
- Universal compatibility - Works in any web environment (React, Vue, Angular, vanilla HTML)
- Secure - JWT in URL hash fragment isn't sent to server or logged
When to use iframe embedding
- Quick integration without adding dependencies
- Non-React applications
- Content management systems (WordPress, Webflow, etc.)
- Simple HTML pages or static sites
- When you don't need programmatic control (filters, callbacks)
When to use React SDK instead
Consider the React SDK if you need:
- Programmatic filters (apply filters via props)
- Callbacks (e.g., onExplore for analytics)
- Seamless React integration
- TypeScript type definitions
For JWT structure and configuration options, see the embedding reference.
iframe URL patterns
All embed URLs follow this pattern: https://your-instance.qyraflow.com/embed/{projectUuid}#{jwtToken}
The JWT is passed in the URL hash fragment (#token) for security—it's not sent to the server in requests or logged in browser history.
Dashboard URL
https://your-instance.qyraflow.com/embed/{projectUuid}#{jwtToken}The dashboardUuid is specified inside the JWT payload as content.dashboardUuid, not in the URL path.
Example:
https://app.qyraflow.com/embed/abc-123-def-456#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Data app URL
Standalone data app embeds put both the project UUID and the app UUID in the path:
https://your-instance.qyraflow.com/embed/{projectUuid}/app/{appUuid}#{jwtToken}The JWT must use content.type: 'dataApp' and the same appUuid as the URL. A token for one app cannot render any other app.
Example:
https://app.qyraflow.com/embed/abc-123-def-456/app/app-uuid-789#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Because the JWT sits in the hash fragment, it is never sent to the server with HTTP requests and never appears in server logs or browser history.
Chart embedding via iframe is not currently supported. Charts can only be embedded using the React SDK.
URL construction
Building the embed URL
- Get your project UUID - Found in Qyra project settings
- Get dashboard ID - Dashboard UUID or slug
- Generate JWT - See embedding reference for token structure
- Construct URL - Combine parts with hash fragment
import jwt from 'jsonwebtoken';
const QYRA_EMBED_SECRET = process.env.QYRA_EMBED_SECRET;
const instanceUrl = 'https://app.qyraflow.com';
const projectUuid = 'your-project-uuid';
const dashboardUuid = 'your-dashboard-uuid';
// Generate JWT
const token = jwt.sign({
content: {
type: 'dashboard',
dashboardUuid: dashboardUuid,
canExportCsv: true,
},
}, QYRA_EMBED_SECRET, { expiresIn: '1h' });
// Build embed URL
const embedUrl = `${instanceUrl}/embed/${projectUuid}#${token}`;
console.log(embedUrl);import jwt
import datetime
QYRA_EMBED_SECRET = os.getenv('QYRA_EMBED_SECRET')
instance_url = 'https://app.qyraflow.com'
project_uuid = 'your-project-uuid'
dashboard_uuid = 'your-dashboard-uuid'
# Generate JWT
payload = {
'content': {
'type': 'dashboard',
'dashboardUuid': dashboard_uuid,
'canExportCsv': True,
},
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(payload, QYRA_EMBED_SECRET, algorithm='HS256')
# Build embed URL
embed_url = f"{instance_url}/embed/{project_uuid}#{token}"
print(embed_url)require 'jwt'
qyra_embed_secret = ENV['QYRA_EMBED_SECRET']
instance_url = 'https://app.qyraflow.com'
project_uuid = 'your-project-uuid'
dashboard_uuid = 'your-dashboard-uuid'
# Generate JWT
payload = {
content: {
type: 'dashboard',
dashboardUuid: dashboard_uuid,
canExportCsv: true
},
exp: Time.now.to_i + 3600
}
token = JWT.encode(payload, qyra_embed_secret, 'HS256')
# Build embed URL
embed_url = "#{instance_url}/embed/#{project_uuid}##{token}"
puts embed_urlURL with user attributes
For row-level security, include user attributes in the JWT:
const token = jwt.sign({
content: {
type: 'dashboard',
dashboardUuid: 'your-dashboard-uuid',
},
userAttributes: {
tenant_id: user.tenantId, // Filter data by tenant
region: user.region,
},
}, QYRA_EMBED_SECRET, { expiresIn: '1h' });
const embedUrl = `https://app.qyraflow.com/embed/${projectUuid}#${token}`;See User attributes reference for complete guide.
URL parameters
Embedded dashboards support a few optional URL parameters that customize the visual appearance and the query timezone of the session. These are added as query parameters before the hash fragment.
URL format
https://your-instance.qyraflow.com/embed/{projectUuid}?theme=dark&backgroundColor=1c1c1c&timezone=America%2FLos_Angeles#{jwtToken}theme
Sets the color scheme for the embedded content.
| Value | Description |
|---|---|
light | Light color scheme (default) |
dark | Dark color scheme |
Example:
https://app.qyraflow.com/embed/abc-123?theme=dark#eyJhbGci...backgroundColor
Sets a custom background color on the embed. Accepts bare hex codes without the # prefix. The # is automatically prepended.
| Format | Example | Description |
|---|---|---|
| 6-digit hex | backgroundColor=121212 | Full hex color |
| 3-digit hex | backgroundColor=FFF | Shorthand hex |
| 8-digit hex | backgroundColor=FF000080 | Hex with alpha |
Only hex color codes are supported. Other CSS color formats (named colors, rgb(), hsl(), etc.) are not supported.
Example:
https://app.qyraflow.com/embed/abc-123?backgroundColor=1c1c1c#eyJhbGci...timezone
Beta Contact support to enable the embed timezone param for your organization. While it is off, the param is ignored and the session falls back to the chart pin or project default. What Beta means.
Sets a per-session query timezone for the embed. Use it when one embedded dashboard needs to render in different timezones for different viewers (for example, a multi-tenant app where each tenant has its own timezone), without creating separate charts per timezone.
Accepts an IANA timezone name (e.g. America/Los_Angeles, Europe/Berlin, Asia/Tokyo). The value is used for dashboard tile queries, inherited totals and subtotals, and filter autocomplete in that session.
URL-encode the value so the / is escaped (America%2FLos_Angeles); the plain America/Los_Angeles form is also accepted. A value that isn't a valid IANA name returns a 400 error.
| Format | Example | Description |
|---|---|---|
| IANA timezone name | timezone=America/Los_Angeles | Renders the session in Los Angeles time |
| IANA timezone name | timezone=Europe/Berlin | Renders the session in Berlin time |
Example:
https://app.qyraflow.com/embed/abc-123?timezone=America%2FLos_Angeles#eyJhbGci...The embed timezone param sits at the top of the timezone resolution priority for that session and overrides any per-chart timezone pin. It only applies to direct iframe and shareable URL embeds, not the React SDK, where the host app's URL is unrelated to the embed.
Combining parameters
You can use theme, backgroundColor, and timezone together:
https://app.qyraflow.com/embed/abc-123?theme=dark&backgroundColor=1c1c1c&timezone=America%2FLos_Angeles#eyJhbGci...All URL parameters are optional. Existing embed URLs without these parameters behave exactly as before — light theme, default background, and the chart or project query timezone.
Building themed URLs
import jwt from 'jsonwebtoken';
const QYRA_EMBED_SECRET = process.env.QYRA_EMBED_SECRET;
const instanceUrl = 'https://app.qyraflow.com';
const projectUuid = 'your-project-uuid';
const dashboardUuid = 'your-dashboard-uuid';
const token = jwt.sign({
content: {
type: 'dashboard',
dashboardUuid: dashboardUuid,
},
}, QYRA_EMBED_SECRET, { expiresIn: '1h' });
// Build embed URL with theming parameters
const embedUrl = `${instanceUrl}/embed/${projectUuid}?theme=dark&backgroundColor=1c1c1c#${token}`;import jwt
import datetime
QYRA_EMBED_SECRET = os.getenv('QYRA_EMBED_SECRET')
instance_url = 'https://app.qyraflow.com'
project_uuid = 'your-project-uuid'
dashboard_uuid = 'your-dashboard-uuid'
payload = {
'content': {
'type': 'dashboard',
'dashboardUuid': dashboard_uuid,
},
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(payload, QYRA_EMBED_SECRET, algorithm='HS256')
# Build embed URL with theming parameters
embed_url = f"{instance_url}/embed/{project_uuid}?theme=dark&backgroundColor=1c1c1c#{token}"Embedding in HTML
Basic iframe
The simplest way to embed is with a standard HTML iframe:
<iframe
src="https://app.qyraflow.com/embed/project-uuid#jwt-token"
width="100%"
height="600"
frameborder="0"
style="border: none;"
></iframe>Recommended attributes
<iframe
src="https://app.qyraflow.com/embed/..."
width="100%"
height="600"
frameborder="0"
style="border: none;"
loading="lazy"
title="Qyra Dashboard"
allowfullscreen
></iframe>Attributes explained:
width="100%"- Makes iframe responsive to container widthheight="600"- Fixed height (adjust based on content)frameborder="0"- Removes default border (legacy)style="border: none;"- Removes border (modern CSS)loading="lazy"- Defers loading until iframe is visibletitle="..."- Accessibility: Describes iframe content for screen readersallowfullscreen- Enables fullscreen mode (if your dashboard uses it)
Responsive iframes
To make iframes maintain aspect ratio and scale responsively:
Method 1: Aspect ratio wrapper (16:9)
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe
src="https://app.qyraflow.com/embed/..."
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
frameborder="0"
allowfullscreen
></iframe>
</div>Method 2: Modern CSS aspect-ratio (16:9)
<iframe
src="https://app.qyraflow.com/embed/..."
style="aspect-ratio: 16/9; width: 100%; border: none;"
frameborder="0"
allowfullscreen
></iframe>Method 3: Fixed viewport percentage
<iframe
src="https://app.qyraflow.com/embed/..."
style="width: 100%; height: 80vh; border: none;"
frameborder="0"
></iframe>Dynamic height
iframes have fixed height by default. For dynamic height based on content:
Qyra embeds do not currently support automatic height adjustment via postMessage. Use a fixed height or viewport-based height (e.g., 80vh).
Recommended approach for dashboards with unknown content:
<iframe
src="https://app.qyraflow.com/embed/..."
style="width: 100%; min-height: 600px; height: 90vh; border: none;"
frameborder="0"
></iframe>Security considerations
iframes provide natural security isolation, but you can add additional restrictions:
<iframe
src="https://app.qyraflow.com/embed/..."
sandbox="allow-scripts allow-same-origin allow-forms allow-downloads"
style="width: 100%; height: 600px; border: none;"
></iframe>sandbox attributes:
allow-scripts- Required for Qyra to functionallow-same-origin- Required for Qyra to functionallow-forms- Required for filter interactionsallow-downloads- Required if you enable CSV/image exports
The sandbox attribute provides additional security but may restrict functionality. Test thoroughly if you use it.
Common patterns
Server-side rendering
Generate embed URLs in your server-side templates:
Express (Node.js)
app.get('/dashboard', authenticateUser, async (req, res) => {
const user = await getUser(req.user.id);
const token = jwt.sign({
content: {
type: 'dashboard',
dashboardUuid: 'dashboard-uuid',
},
userAttributes: {
tenant_id: user.tenantId,
},
}, process.env.QYRA_EMBED_SECRET, { expiresIn: '1h' });
const embedUrl = `https://app.qyraflow.com/embed/${projectUuid}#${token}`;
res.render('dashboard', { embedUrl });
});Template (EJS)
<div class="dashboard-container">
<iframe
src="<%= embedUrl %>"
width="100%"
height="600"
frameborder="0"
style="border: none;"
></iframe>
</div>Single-page apps (SPA)
Generate URLs via API when component mounts:
React example
function EmbeddedDashboard() {
const [embedUrl, setEmbedUrl] = useState(null);
useEffect(() => {
fetch('/api/dashboard-embed-url')
.then(res => res.json())
.then(data => setEmbedUrl(data.url));
}, []);
if (!embedUrl) return <div>Loading...</div>;
return (
<iframe
src={embedUrl}
width="100%"
height="600"
frameBorder="0"
style={{ border: 'none' }}
/>
);
}Static sites
For static sites, generate URLs at build time or use edge functions:
Next.js (server component)
import jwt from 'jsonwebtoken';
async function DashboardPage() {
// Generate at request time
const token = jwt.sign({
content: {
type: 'dashboard',
dashboardUuid: 'dashboard-uuid',
},
}, process.env.QYRA_EMBED_SECRET, { expiresIn: '24h' });
const embedUrl = `https://app.qyraflow.com/embed/project-uuid#${token}`;
return (
<iframe
src={embedUrl}
width="100%"
height="600"
frameBorder="0"
style={{ border: 'none' }}
/>
);
}Content management systems
Embed in WordPress, Webflow, or other CMS:
- Create a server endpoint that generates embed URLs
- Use iframe embed code with dynamic URL
- Refresh tokens via JavaScript when expired
WordPress shortcode example:
function qyra_embed_shortcode($atts) {
$atts = shortcode_atts(array(
'dashboard' => '',
), $atts);
$embed_url = generate_qyra_url($atts['dashboard']);
return '<iframe src="' . esc_url($embed_url) . '" width="100%" height="600" frameborder="0" style="border: none;"></iframe>';
}
add_shortcode('qyra', 'qyra_embed_shortcode');Token refresh
JWTs expire after the time specified in expiresIn. Handle token expiration:
Option 1: Long-lived tokens
For public or semi-public dashboards, use longer expiration:
jwt.sign(payload, secret, { expiresIn: '7d' }) // 7 daysLong-lived tokens are convenient but less secure. Use only when appropriate for your use case.
Option 2: Regenerate URL on expiration
Detect when iframe shows "Token expired" error and reload with new URL:
function refreshEmbed() {
fetch('/api/dashboard-embed-url')
.then(res => res.json())
.then(data => {
document.getElementById('dashboard-iframe').src = data.url;
});
}
// Refresh before expiration (e.g., every 50 minutes for 1-hour tokens)
setInterval(refreshEmbed, 50 * 60 * 1000);Option 3: Backend proxy
Create a backend endpoint that serves a static iframe URL but generates fresh tokens:
app.get('/embed-proxy/dashboard/:dashboardUuid', authenticateUser, (req, res) => {
const token = jwt.sign({
content: {
type: 'dashboard',
dashboardUuid: req.params.dashboardUuid,
},
}, process.env.QYRA_EMBED_SECRET, { expiresIn: '1h' });
const embedUrl = `https://app.qyraflow.com/embed/${projectUuid}#${token}`;
// Redirect to actual embed URL
res.redirect(embedUrl);
});Then use:
<iframe src="/embed-proxy/dashboard/dashboard-uuid"></iframe>Troubleshooting
Token not working
Issue: iframe shows "Invalid token" or "Token expired"
Solutions:
- Verify embed secret matches between token generation and Qyra
- Check token hasn't expired (
expiresInin jwt.sign) - Ensure JWT payload structure matches embedding reference
- Test token expiration:
jwt.decode(token)and checkexpfield
Content not displaying
Issue: iframe is blank or shows loading indefinitely
Solutions:
- Check browser console for errors
- Verify dashboard/chart UUID is correct
- Ensure content is added to "allowed dashboards/charts" in Qyra settings
- Check project UUID is correct
- Try accessing embed URL directly in browser to see error message
CORS errors
Issue: Browser console shows CORS errors
Solution:
- iframes should NOT have CORS issues (CORS only affects React SDK)
- If you see CORS errors with iframes, you may be using fetch/XHR to load content instead of iframe
- Use standard iframe src attribute, not JavaScript-based loading
URL encoding issues
Issue: JWT or URL appears malformed
Solutions:
- Don't URL-encode the JWT in the hash fragment
- If constructing URLs in templates, ensure proper escaping:
<!-- Good --> <iframe src="https://app.qyraflow.com/embed/...#<%= token %>"></iframe> <!-- Bad - Don't URL encode token --> <iframe src="https://app.qyraflow.com/embed/...#<%= encodeURIComponent(token) %>"></iframe>
Dashboard filters not working
Issue: Users can't interact with filters despite dashboardFiltersInteractivity: { enabled: 'all' }
Solutions:
- Verify JWT includes correct interactivity settings
- Check browser console for JavaScript errors
- Ensure iframe isn't using
sandboxattribute withoutallow-forms