Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power BI

Mastering Power BI Embedded Analytics: Integrating Reports into Web Applications Using Service Principal Authentication and the JavaScript API

Learn how to embed fully interactive Power BI reports into web applications using service principal authentication, the Power BI REST API, and the powerbi-client JavaScript SDK. Covers multi-tenant RLS enforcement, token refresh, and programmatic report control from filter application to bookmark management.

🔥 Expert29 min readSep 13, 2026Updated Sep 13, 2026
Mastering Power BI Embedded Analytics: Integrating Reports into Web Applications Using Service Principal Authentication and the JavaScript API
On this page
  • Introduction
  • Prerequisites
  • The Architecture You Need to Understand First
  • Step 1: Registering a Service Principal in Microsoft Entra ID
  • Step 2: Adding the Service Principal to the Power BI Workspace
  • Step 3: Generating the Embed Token Server-Side
  • Step 4: Embedding Reports with the JavaScript API
  • Step 5: Controlling Reports Programmatically
  • Applying Filters
  • Page Navigation
  • Bookmark Interactions
  • Visual-Level Interactions
  • Step 6: Implementing Effective Identity for Multi-Tenant Security
  • Step 7: Token Refresh and Production Considerations
  • Performance Optimization
  • Step 8: Advanced Embedding Patterns
  • Embedding Multiple Reports with a Single Token
  • Embedding a Single Visual
  • Responding to Cross-Filter Events
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • "403 Forbidden" when generating the embed token
  • Report loads but shows "access denied" or blank
  • RLS not being enforced (users see all data)
  • "The effective identity doesn't correspond to any RLS roles"
  • `powerbi.embed()` returns undefined or doesn't render
  • Filters throw "invalid filter" errors
  • Token expiry handling fails silently
  • Security Checklist
  • Summary & Next Steps
  • Mastering Power BI Embedded Analytics: Integrating Reports into Web Applications Using Service Principal Authentication and the JavaScript API

    Introduction

    Picture this: your company has built a genuinely excellent Power BI report — clean data model, well-designed visuals, meaningful DAX measures — and now your product team wants it living inside the customer-facing web application. Not linked out to the Power BI service, not in an iframe that breaks on mobile, but actually embedded, white-labeled, responding to user interactions in the app, and secured so that customer A never sees customer B's data. That's a real engineering problem, and it's harder than it looks the first time you encounter it.

    Power BI Embedded Analytics is Microsoft's answer to exactly this problem. It lets you programmatically generate access tokens, render fully interactive reports inside a div element in your web application, and control those reports through a JavaScript API that gives you fine-grained control over filters, bookmarks, page navigation, and events. The catch is that the setup involves Azure Active Directory (now Microsoft Entra ID), service principals, Power BI workspaces, capacity licensing, and a JavaScript SDK — all of which have to work together correctly before a single pixel renders. When one of those pieces is misconfigured, you get a blank div and an unhelpful error message.

    By the end of this lesson, you will know how to build a complete embedded analytics integration from scratch. You'll understand the authentication flow deeply enough to debug it when it breaks, control embedded reports programmatically through the JavaScript API, apply dynamic filters from your application's context, and implement the Row-Level Security patterns that make multi-tenant embedded analytics actually secure. Here are the specific things you'll be able to do:

    What you'll learn:

    • Configure a service principal in Azure/Entra ID and grant it the correct Power BI workspace permissions
    • Generate embed tokens using the Power BI REST API from a server-side application
    • Embed reports, dashboards, and Q&A visuals using the powerbi-client JavaScript library
    • Control embedded reports programmatically — apply filters, switch pages, capture events, and call bookmarks
    • Implement Effective Identity (Row-Level Security) in embedded contexts for multi-tenant data isolation
    • Handle token refresh, error states, and performance optimization in production deployments

    Prerequisites

    This lesson assumes you're comfortable with the following:

    • You understand Power BI workspaces, datasets, and reports. If you need a refresher on how the service organizes content, see Understanding the Power BI Ecosystem: Desktop, Service, Mobile, and Gateway Explained.
    • You've published at least one report to the Power BI service. The process is covered in Publishing and Sharing Reports in Power BI Service: From Desktop to Enterprise Distribution.
    • Basic familiarity with REST APIs, HTTP concepts, and either JavaScript/Node.js or C#/.NET for the server-side component.
    • You understand Row-Level Security in Power BI at a conceptual level. For a deep dive on RLS configuration, see Implementing Row-Level Security in Power BI: Dynamic Rules, Role Testing, and Enterprise Deployment.
    • You have access to an Azure subscription where you can register applications and an account with Power BI Pro or Premium capacity.

    The Architecture You Need to Understand First

    Before touching a single line of code, you need a clear mental model of how Power BI Embedded authentication works. Most integration failures happen because someone skips this step and starts copying token-generation code without understanding what it's actually doing.

    There are two embedding approaches Microsoft documents:

    Embed for your organization ("User owns data"): The end user authenticates with their own Power BI account. The report token is tied to that user's permissions. This is appropriate for internal portals where all users have Power BI licenses.

    Embed for your customers ("App owns data"): Your application authenticates as a service principal. You generate embed tokens on behalf of your users. This is the approach used for external-facing products, ISV solutions, and any scenario where your end users don't have Power BI accounts. It's also the harder one to set up, and it's what we'll focus on in this lesson.

    The token flow for "App owns data" looks like this:

    1. Your web application's server-side component (Node.js, .NET, Python — your choice) authenticates against Microsoft Entra ID using a client ID and client secret (or certificate) to obtain an AAD access token.
    2. Using that AAD access token, your server calls the Power BI REST API to generate an embed token — a short-lived, scoped credential that authorizes embedding of a specific report.
    3. The embed token, along with the embed URL and report ID, is returned to your front-end JavaScript code.
    4. Your front-end uses the powerbi-client JavaScript SDK to render the report inside a container element, passing it the embed token and URL.
    5. The browser communicates directly with Power BI service endpoints for the actual data rendering — your server doesn't proxy that traffic.

    The service principal is never exposed to the browser. It lives only on your server. This is a critical architectural point: if you ever find yourself putting your client secret or AAD access token into front-end JavaScript, something has gone badly wrong.

    Warning: Never expose your service principal's client secret or the AAD access token to the browser. These credentials allow anyone who obtains them to call the Power BI REST API with full permissions. The embed token is the only credential that should reach the client side, and even that should be treated as sensitive.


    Step 1: Registering a Service Principal in Microsoft Entra ID

    Open the Azure portal and navigate to Microsoft Entra ID (formerly Azure Active Directory). In the left navigation, select App registrations, then New registration.

    Give your application a descriptive name — something like PowerBI-Embedded-Prod rather than MyApp. For supported account types, choose Accounts in this organizational directory only unless you have a specific reason for multi-tenant. Leave the redirect URI blank for now; service principals using client credentials don't use redirect URIs.

    After registration, copy three values from the application's Overview page — you'll need all three:

    • Application (client) ID — this is your clientId
    • Directory (tenant) ID — this is your tenantId
    • Object ID — you'll need this when adding the service principal to Power BI workspaces

    Next, create a client secret. Go to Certificates & secrets, select Client secrets, then New client secret. Set an expiry that matches your organization's key rotation policies (12 or 24 months, with a reminder to rotate before expiry). Copy the secret value immediately — you cannot retrieve it again after navigating away.

    Tip: In production systems, prefer certificate-based authentication over client secrets. Certificates are harder to accidentally leak through log files or environment variable dumps, and they support HSM-backed key storage in Azure Key Vault. The MSAL libraries support certificate thumbprint authentication with nearly identical code to secret-based auth.

    Now configure API permissions. In your app registration, select API permissions, then Add a permission. Choose Power BI Service from the list of Microsoft APIs. Select Application permissions (not delegated — delegated permissions are for user-context auth). Add these permissions:

    • Dataset.ReadWrite.All
    • Report.ReadWrite.All
    • Workspace.Read.All

    After adding them, click Grant admin consent for [your tenant]. Without admin consent, the application permissions won't work.

    Finally, you need to enable service principal access in the Power BI admin portal. Sign in to app.powerbi.com, navigate to Settings (gear icon) > Admin portal > Tenant settings. Find Developer settings and within it, Allow service principals to use Power BI APIs. Enable this setting, and either apply it to the entire organization or to a specific security group that contains your registered application.


    Step 2: Adding the Service Principal to the Power BI Workspace

    Your service principal now has permission to call Power BI APIs, but it still needs access to the specific workspace containing the reports you want to embed.

    Navigate to the workspace in Power BI service, select Workspace settings, then the Access tab. Add the service principal by searching for its application name (the one you used in Entra ID). Assign it the Member or Contributor role. The Viewer role is not sufficient for generating embed tokens — the service principal needs at least Member level access.

    Note: If your organization uses Premium capacity (P-SKU or A-SKU), make sure the workspace is assigned to that capacity before attempting to generate embed tokens. Embed tokens for external users require Premium capacity or Power BI Embedded capacity (A-SKU). Trying to embed without capacity assignment will result in a PowerBINotAuthorizedException or a 403 response from the token generation API.

    Collect the workspace ID and report ID. In Power BI service, navigate to your report and look at the URL. It follows the pattern:

    https://app.powerbi.com/groups/{workspaceId}/reports/{reportId}/ReportSection
    

    Both GUIDs in that URL are what you need. Save them — you'll pass them to the embed token API.


    Step 3: Generating the Embed Token Server-Side

    Now we build the server component. I'll show this in Node.js since it's the most common web application language, but the patterns translate directly to C#, Python, or any other language with HTTP client capabilities.

    Install the required packages:

    npm install @azure/msal-node node-fetch express
    

    Here's a complete embed token service. Notice how each step maps to the architecture we described:

    // embedService.js
    const msal = require('@azure/msal-node');
    
    const msalConfig = {
      auth: {
        clientId: process.env.POWERBI_CLIENT_ID,
        clientSecret: process.env.POWERBI_CLIENT_SECRET,
        authority: `https://login.microsoftonline.com/${process.env.POWERBI_TENANT_ID}`,
      },
    };
    
    const cca = new msal.ConfidentialClientApplication(msalConfig);
    const POWERBI_SCOPE = ['https://analysis.windows.net/powerbi/api/.default'];
    
    async function getAadToken() {
      const result = await cca.acquireTokenByClientCredential({
        scopes: POWERBI_SCOPE,
      });
      return result.accessToken;
    }
    
    async function getEmbedToken(workspaceId, reportId, aadToken, effectiveIdentity = null) {
      const reportUrl = `https://api.powerbi.com/v1.0/myorg/groups/${workspaceId}/reports/${reportId}`;
      
      // First, get the report metadata to find the dataset ID
      const reportResponse = await fetch(reportUrl, {
        headers: { Authorization: `Bearer ${aadToken}` },
      });
      
      if (!reportResponse.ok) {
        const errorBody = await reportResponse.text();
        throw new Error(`Failed to fetch report metadata: ${reportResponse.status} - ${errorBody}`);
      }
      
      const reportData = await reportResponse.json();
      const datasetId = reportData.datasetId;
      const embedUrl = reportData.embedUrl;
    
      // Build the token request body
      const tokenRequestBody = {
        reports: [{ id: reportId }],
        datasets: [{ id: datasetId }],
        targetWorkspaces: [{ id: workspaceId }],
      };
    
      // Add effective identity for Row-Level Security
      if (effectiveIdentity) {
        tokenRequestBody.identities = [
          {
            username: effectiveIdentity.username,
            roles: effectiveIdentity.roles,
            datasets: [datasetId],
          },
        ];
      }
    
      const tokenResponse = await fetch(
        'https://api.powerbi.com/v1.0/myorg/GenerateToken',
        {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${aadToken}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(tokenRequestBody),
        }
      );
    
      if (!tokenResponse.ok) {
        const errorBody = await tokenResponse.text();
        throw new Error(`Failed to generate embed token: ${tokenResponse.status} - ${errorBody}`);
      }
    
      const tokenData = await tokenResponse.json();
    
      return {
        token: tokenData.token,
        tokenId: tokenData.tokenId,
        expiration: tokenData.expiration,
        embedUrl: embedUrl,
        reportId: reportId,
      };
    }
    
    module.exports = { getAadToken, getEmbedToken };
    

    Now wire this into an Express endpoint:

    // server.js
    const express = require('express');
    const { getAadToken, getEmbedToken } = require('./embedService');
    
    const app = express();
    app.use(express.json());
    
    // This endpoint is called by your front-end to get embed credentials
    // It should be protected by your own application's authentication middleware
    app.get('/api/embed-config', async (req, res) => {
      try {
        // In a real application, authenticate the requesting user here
        // and determine their effective identity/roles before calling getEmbedToken
        const currentUser = req.user; // from your auth middleware
        
        const workspaceId = process.env.POWERBI_WORKSPACE_ID;
        const reportId = process.env.POWERBI_REPORT_ID;
    
        const aadToken = await getAadToken();
        
        // Map your application's user to Power BI RLS identity
        const effectiveIdentity = {
          username: currentUser.email,
          roles: currentUser.pbiRoles, // e.g., ['RegionManager', 'EMEA']
        };
    
        const embedConfig = await getEmbedToken(
          workspaceId,
          reportId,
          aadToken,
          effectiveIdentity
        );
    
        res.json(embedConfig);
      } catch (error) {
        console.error('Embed token generation failed:', error.message);
        res.status(500).json({ error: 'Failed to generate embed configuration' });
      }
    });
    
    app.listen(3000, () => console.log('Server running on port 3000'));
    

    Notice the pattern: the effectiveIdentity object is constructed from your application's authenticated user context, not from anything the browser sends. This is important. If your application allows the browser to dictate which RLS roles apply, you've created a privilege escalation vulnerability.


    Step 4: Embedding Reports with the JavaScript API

    With the server providing embed tokens, we can now build the front-end. Install the Power BI JavaScript client library:

    npm install powerbi-client
    

    Or via CDN in an HTML page:

    <script src="https://cdn.jsdelivr.net/npm/powerbi-client@2.23.0/dist/powerbi.min.js"></script>
    

    The powerbi-client library exports a service.Service class and a global powerbi object. Here's a complete embedding implementation that demonstrates all the key patterns:

    <!-- In your HTML -->
    <div id="report-container" style="height: 600px; width: 100%;"></div>
    
    // embedReport.js
    import * as pbi from 'powerbi-client';
    
    const powerbi = new pbi.service.Service(
      pbi.factories.hpmFactory,
      pbi.factories.wpmpFactory,
      pbi.factories.routerFactory
    );
    
    let embeddedReport = null;
    
    async function fetchEmbedConfig() {
      const response = await fetch('/api/embed-config', {
        headers: {
          // Include your application's auth token here
          Authorization: `Bearer ${getAppToken()}`,
        },
      });
      
      if (!response.ok) {
        throw new Error(`Failed to fetch embed config: ${response.status}`);
      }
      
      return response.json();
    }
    
    async function embedReport() {
      const embedConfig = await fetchEmbedConfig();
      
      const reportContainer = document.getElementById('report-container');
    
      const config = {
        type: 'report',
        id: embedConfig.reportId,
        embedUrl: embedConfig.embedUrl,
        accessToken: embedConfig.token,
        tokenType: pbi.models.TokenType.Embed, // NOT Aad — this is the embed token
        settings: {
          panes: {
            filters: { expanded: false, visible: true },
            pageNavigation: { visible: false }, // hide default page nav, we'll build our own
          },
          background: pbi.models.BackgroundType.Transparent,
          navContentPaneEnabled: false,
        },
      };
    
      // Embed the report and store the reference
      embeddedReport = powerbi.embed(reportContainer, config);
    
      // Attach event handlers
      embeddedReport.on('loaded', handleReportLoaded);
      embeddedReport.on('rendered', handleReportRendered);
      embeddedReport.on('error', handleReportError);
      embeddedReport.on('dataSelected', handleDataSelected);
    
      return embeddedReport;
    }
    
    async function handleReportLoaded() {
      console.log('Report loaded');
      // The report DOM is ready but data may still be loading
      // Good time to apply initial filters
      await applyInitialFilters();
    }
    
    async function handleReportRendered() {
      console.log('Report fully rendered with data');
      // Hide your loading spinner here
      document.getElementById('loading-spinner').style.display = 'none';
    }
    
    function handleReportError(event) {
      const error = event.detail;
      console.error('Embedded report error:', error);
      // Log to your telemetry system
      // Display a user-friendly error message
    }
    
    async function handleDataSelected(event) {
      const data = event.detail;
      console.log('User selected data:', data);
      // You can use this to drive interactions in other parts of your UI
      // For example, updating a sidebar with details about the selected data point
    }
    
    export { embedReport, embeddedReport };
    

    Key insight: The loaded event fires when the report structure is initialized but before data has been fetched and rendered. The rendered event fires when the first complete render with data is done. Apply filters on loaded, hide loading spinners on rendered. Getting this sequence wrong leads to race conditions where filters are applied before the report is ready.


    Step 5: Controlling Reports Programmatically

    This is where embedded analytics gets genuinely powerful. The powerbi-client API lets your application control the embedded report — not just display it.

    Applying Filters

    You can apply report-level, page-level, or visual-level filters programmatically. The filter objects follow the same schema as Power BI's internal filter model:

    async function applyInitialFilters() {
      if (!embeddedReport) return;
    
      // Basic filter: show only data for a specific region
      const regionFilter = {
        $schema: 'http://powerbi.com/product/schema#basic',
        target: {
          table: 'Geography',
          column: 'Region',
        },
        operator: 'In',
        values: ['North America', 'EMEA'],
        filterType: pbi.models.FilterType.Basic,
      };
    
      // Advanced filter: date range using two conditions
      const dateFilter = {
        $schema: 'http://powerbi.com/product/schema#advanced',
        target: {
          table: 'Date',
          column: 'DateKey',
        },
        logicalOperator: 'And',
        conditions: [
          { operator: 'GreaterThanOrEqual', value: 20230101 },
          { operator: 'LessThanOrEqual', value: 20231231 },
        ],
        filterType: pbi.models.FilterType.Advanced,
      };
    
      try {
        // Apply multiple filters at once — more efficient than applying one by one
        await embeddedReport.setFilters([regionFilter, dateFilter]);
        console.log('Filters applied successfully');
      } catch (error) {
        console.error('Failed to apply filters:', error);
      }
    }
    
    // Remove all filters
    async function clearAllFilters() {
      if (!embeddedReport) return;
      await embeddedReport.removeFilters();
    }
    
    // Update a single filter without touching others
    async function filterByCustomer(customerId) {
      if (!embeddedReport) return;
      
      const customerFilter = {
        $schema: 'http://powerbi.com/product/schema#basic',
        target: {
          table: 'Customers',
          column: 'CustomerID',
        },
        operator: 'In',
        values: [customerId],
        filterType: pbi.models.FilterType.Basic,
      };
    
      // Get current filters, add or update the customer filter
      const currentFilters = await embeddedReport.getFilters();
      const otherFilters = currentFilters.filter(
        f => !(f.target?.table === 'Customers' && f.target?.column === 'CustomerID')
      );
      
      await embeddedReport.setFilters([...otherFilters, customerFilter]);
    }
    

    The table and column values in filter targets must match the names in your Power BI data model exactly — not the display names you might have set in the report. If you've renamed a column in Power Query or the model, use the model name.

    Page Navigation

    If you've hidden the built-in page navigation pane (which you often will in embedded scenarios to maintain your app's navigation structure), you need to handle page switching programmatically:

    async function getReportPages() {
      if (!embeddedReport) return [];
      const pages = await embeddedReport.getPages();
      return pages.map(page => ({
        name: page.name,           // internal page name (e.g., "ReportSection3")
        displayName: page.displayName, // human-readable name
        isActive: page.isActive,
      }));
    }
    
    async function navigateToPage(pageName) {
      if (!embeddedReport) return;
      
      const pages = await embeddedReport.getPages();
      const targetPage = pages.find(p => p.name === pageName);
      
      if (!targetPage) {
        console.error(`Page "${pageName}" not found in report`);
        return;
      }
      
      await targetPage.setActive();
    }
    
    // Build a custom navigation tab bar
    async function buildCustomNavigation() {
      const pages = await getReportPages();
      const navContainer = document.getElementById('report-nav');
      navContainer.innerHTML = '';
      
      pages.forEach(page => {
        const tab = document.createElement('button');
        tab.textContent = page.displayName;
        tab.classList.toggle('active', page.isActive);
        tab.addEventListener('click', () => navigateToPage(page.name));
        navContainer.appendChild(tab);
      });
    }
    

    For more sophisticated page flow patterns, the same concepts you use within Power BI Desktop — like the bookmark-based navigation patterns described in Build Professional Navigation in Power BI: Bookmarks, Buttons & Page Flow Mastery — can be triggered programmatically through the JavaScript API, giving you a unified navigation system that spans your app and the embedded report.

    Bookmark Interactions

    async function captureCurrentBookmark() {
      if (!embeddedReport) return null;
      const capturedBookmark = await embeddedReport.bookmarksManager.capture();
      return capturedBookmark.state; // a string you can store and restore later
    }
    
    async function applyBookmark(bookmarkState) {
      if (!embeddedReport) return;
      await embeddedReport.bookmarksManager.applyState(bookmarkState);
    }
    
    // Apply a named bookmark defined in the report
    async function applyNamedBookmark(bookmarkName) {
      if (!embeddedReport) return;
      const bookmarks = await embeddedReport.bookmarksManager.getBookmarks();
      const target = bookmarks.find(b => b.name === bookmarkName);
      
      if (target) {
        await embeddedReport.bookmarksManager.apply(target.name);
      }
    }
    

    This is useful for building "saved view" functionality in your application — capture a user's current filter state and bookmark position, store it in your database, and restore it the next time they visit.

    Visual-Level Interactions

    The API goes down to individual visuals when needed:

    async function getVisualData(pageName, visualName) {
      if (!embeddedReport) return null;
      
      const pages = await embeddedReport.getPages();
      const page = pages.find(p => p.name === pageName);
      
      if (!page) return null;
      
      const visuals = await page.getVisuals();
      const targetVisual = visuals.find(v => v.name === visualName);
      
      if (!targetVisual) return null;
      
      // Export visual data as a result object
      const result = await targetVisual.exportData(pbi.models.ExportDataType.Summarized);
      return result.data; // CSV string of the summarized data
    }
    

    Step 6: Implementing Effective Identity for Multi-Tenant Security

    The most critical security capability in embedded analytics is Effective Identity — the mechanism that tells Power BI "render this report as if this user is logged in, with these RLS roles applied."

    If your application serves multiple customers (multi-tenant), and each customer should only see their own data, Effective Identity combined with Row-Level Security in your Power BI dataset is how you enforce that isolation. The RLS roles must be defined in your dataset before the embed token can reference them — that configuration is done in Power BI Desktop and implementing those dynamic RLS rules is a prerequisite to this step.

    Here's the trust model:

    • Power BI trusts the service principal completely. Whatever effective identity the service principal declares in the token generation call is accepted.
    • This means your server-side code is the security boundary. It must map authenticated users to appropriate identities and roles — and it must not allow clients to influence this mapping.
    // A more complete effective identity implementation
    function buildEffectiveIdentity(authenticatedUser) {
      // authenticatedUser comes from your application's own auth system
      // (JWT claims, session data, etc.) — never from request parameters
      
      const identity = {
        username: authenticatedUser.email,
        roles: [],
        datasets: [process.env.POWERBI_DATASET_ID],
      };
    
      // Map your application's permission model to Power BI RLS roles
      if (authenticatedUser.organizationId) {
        // Use a dynamic username that matches your DAX RLS expression
        // The DAX role filter might be: [OrganizationID] = USERNAME()
        identity.username = authenticatedUser.organizationId.toString();
        identity.roles = ['OrganizationMember'];
      }
    
      if (authenticatedUser.isRegionalManager) {
        identity.roles.push('RegionalManager');
        // RLS role might restrict to manager's assigned regions
      }
    
      if (authenticatedUser.isSuperAdmin) {
        // Super admins see all data — don't add any restricting roles
        // But you still need to provide an identity; use a special bypass role
        // that has no WHERE clause in its DAX filter
        identity.roles = ['GlobalAdmin'];
      }
    
      return identity;
    }
    

    Warning: If your Power BI dataset uses dynamic RLS rules — where the DAX expression uses USERNAME() or USERPRINCIPALNAME() to filter data — then the username field in the effective identity is what gets returned by those DAX functions. Design your identity mapping carefully. If you pass an email address, your DAX must compare against email addresses. If you pass a customer ID integer as a string, your DAX must compare against that. Consistency between your token generation code and your DAX rules is essential.

    A common pattern for multi-tenant ISV scenarios is to pass the tenant/organization ID as the username and use a single TenantMember role with a DAX filter like this in your Power BI dataset:

    // In the Power BI dataset RLS role filter for the 'Organizations' table:
    [OrganizationID] = VALUE(USERNAME())
    

    This means every tenant's data is filtered to their organization's records, and you never have to create new RLS roles for new customers — just pass their organization ID as the username when generating the token.


    Step 7: Token Refresh and Production Considerations

    Embed tokens expire. The default expiry is one hour, though the exact expiry is returned in the expiration field of the token generation response. If a user is viewing an embedded report when the token expires, the report will stop refreshing and eventually display an error.

    The powerbi-client library fires a tokenExpired event before the token actually expires, giving you a window to refresh it silently:

    embeddedReport.on('tokenExpired', async () => {
      console.log('Embed token expiring, refreshing...');
      
      try {
        const newConfig = await fetchEmbedConfig();
        await embeddedReport.setAccessToken(newConfig.token);
        console.log('Token refreshed successfully');
      } catch (error) {
        console.error('Failed to refresh embed token:', error);
        // Show user a message and offer a page reload
        showTokenRefreshError();
      }
    });
    

    setAccessToken() replaces the token without re-embedding the report or losing the user's current state (active page, applied filters, etc.). This is much better than destroying and re-embedding the report.

    Tip: The tokenExpired event fires approximately 2 minutes before the actual expiry. Use this window to call your server, generate a new token, and call setAccessToken(). If your server's token generation is slow (due to cold AAD authentication), consider caching your AAD access token server-side. AAD tokens are valid for one hour and can be cached and reused across many embed token requests.

    Performance Optimization

    A few production-critical optimizations:

    Cache AAD tokens server-side. The MSAL library does this automatically in memory, but if you're running multiple server instances (which you should be in production), use a distributed cache like Redis. Fetching a new AAD token adds ~200-400ms to every token generation request.

    Use the GenerateToken V2 API for bulk operations. If your page embeds multiple reports, the V2 API (/v1.0/myorg/GenerateToken) accepts multiple reports and datasets in a single request, returning a single token that works for all of them. This is far more efficient than making N sequential token generation calls.

    Pre-fetch embed config. Don't wait for the user to click on a report before fetching the embed config. If you know the user will need an embedded report on page load, fetch the config in parallel with your other page data.

    Consider the first render time. Power BI reports can take 2-8 seconds to initially render, depending on the data model complexity and whether caching is warm. Always show a loading state. For DirectQuery models, review the guidance in Optimizing Power BI Report Performance: Query Reduction, Aggregations, and DirectQuery Tuning — every optimization you make in the dataset directly improves the embedded experience too.


    Step 8: Advanced Embedding Patterns

    Embedding Multiple Reports with a Single Token

    // Server-side: Generate one token for multiple reports
    async function getMultiReportEmbedToken(workspaceId, reportConfigs, aadToken) {
      const tokenRequestBody = {
        reports: reportConfigs.map(r => ({ id: r.reportId })),
        datasets: [...new Set(reportConfigs.map(r => r.datasetId))].map(id => ({ id })),
        targetWorkspaces: [{ id: workspaceId }],
      };
    
      const tokenResponse = await fetch(
        'https://api.powerbi.com/v1.0/myorg/GenerateToken',
        {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${aadToken}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(tokenRequestBody),
        }
      );
    
      const tokenData = await tokenResponse.json();
      
      // Return the single token to be used for all report embeds
      return tokenData.token;
    }
    

    On the front end, all your powerbi.embed() calls use the same token value, and Power BI service handles routing correctly.

    Embedding a Single Visual

    In some scenarios you want to embed a single visual from a report rather than the entire report — useful for embedding a KPI card or a specific chart inline with your application's own content:

    async function embedSingleVisual(containerElement, reportId, pageName, visualName) {
      const embedConfig = await fetchEmbedConfig(); // your server endpoint
      
      const config = {
        type: 'visual',
        id: reportId,
        embedUrl: embedConfig.embedUrl,
        accessToken: embedConfig.token,
        tokenType: pbi.models.TokenType.Embed,
        pageName: pageName,     // internal page name
        visualName: visualName, // internal visual name (GUID-like string)
        settings: {
          background: pbi.models.BackgroundType.Transparent,
        },
      };
    
      return powerbi.embed(containerElement, config);
    }
    

    The visual name is the internal name assigned by Power BI, which you can find through the getVisuals() API call or by inspecting the report with the JavaScript API.

    Responding to Cross-Filter Events

    One of the most powerful integration patterns is driving your application's UI in response to user interactions inside the embedded report. For example, when a user clicks a bar in a sales chart, your app could update a details panel outside the report iframe:

    embeddedReport.on('dataSelected', async (event) => {
      const selection = event.detail;
      
      if (!selection || !selection.dataPoints || selection.dataPoints.length === 0) {
        // User deselected — reset the sidebar
        resetDetailSidebar();
        return;
      }
    
      // Extract the selected values from the event
      const selectedValues = selection.dataPoints.map(dp => {
        const identities = dp.identity;
        return identities.reduce((acc, identity) => {
          acc[identity.target.column] = identity.equals;
          return acc;
        }, {});
      });
    
      // Call your own API with the selected context
      const details = await fetchDetailsForSelection(selectedValues);
      updateDetailSidebar(details);
    });
    

    This bidirectional integration — where user actions in the Power BI report drive behavior in the surrounding application — is what elevates embedded analytics from "an iframe with a report" to a genuinely integrated experience.


    Hands-On Exercise

    Build a complete embedded analytics prototype using the following scenario:

    Scenario: You're building a customer portal for a SaaS company. Each customer (tenant) should see only their own usage and billing data in an embedded Power BI report. The portal has its own navigation, so the report's built-in navigation panes should be hidden. Customers should be able to download a PDF of the report using a button in your app's UI.

    Exercise steps:

    1. Create the dataset and report. In Power BI Desktop, build a simple report using a data model that includes a CustomerID column on your main fact table. Create an RLS role called CustomerView with the DAX filter [CustomerID] = VALUE(USERNAME()) on the fact table. Publish to a workspace.

    2. Set up service principal authentication. Register an app in Entra ID, create a client secret, grant Power BI API permissions with admin consent, enable service principal access in the Power BI admin portal, and add the service principal as a workspace Member.

    3. Build the server endpoint. Create a Node.js Express server with an /api/embed-config endpoint. The endpoint should accept the authenticated user's customer ID from your application's session (not from the request body — imagine it comes from a verified JWT), and pass it as the username in the effective identity with the CustomerView role.

    4. Build the front-end embedding. Create an HTML page with a container div for the report. On page load, fetch the embed config from your server and embed the report using powerbi-client. Hide all panes. Build a custom navigation bar that reads page names from the getPages() API. Handle the tokenExpired event with a silent refresh.

    5. Add an export button. When the user clicks "Export to PDF" in your app's UI, call the Power BI REST API's export endpoint to trigger an asynchronous export. Poll for completion and download the file. The export API endpoint is POST /v1.0/myorg/groups/{groupId}/reports/{reportId}/ExportTo with a body of { "format": "PDF" }.

    6. Test the RLS isolation. Simulate two different customer sessions by changing the customer ID you pass as the effective identity username. Verify that each session sees only the correct customer's data.

    Success criteria: The report loads within a reasonable time, page navigation works through your custom nav bar, the token refresh happens silently when the token expires (you can test this by reducing the expiry), and switching the customer ID in your test session shows a completely different data set.


    Common Mistakes & Troubleshooting

    "403 Forbidden" when generating the embed token

    This is the most common error and has three likely causes:

    1. Service principal not added to the workspace. Go to the workspace access settings and verify the service principal is listed with at least Member role.
    2. Service principal API access not enabled in the admin portal. Verify under Tenant settings > Developer settings.
    3. Admin consent not granted. In the app registration's API permissions, verify that admin consent has been granted (the status column should show green checkmarks, not warning icons).

    Report loads but shows "access denied" or blank

    This usually means the dataset isn't in a workspace with Premium or Embedded capacity. Check the workspace's capacity assignment.

    RLS not being enforced (users see all data)

    Verify that the RLS role is defined on the dataset (not just the report), and that your effective identity's roles array exactly matches the role name as defined in Power BI Desktop. Role names are case-sensitive in this context.

    "The effective identity doesn't correspond to any RLS roles"

    This error from the token generation API means the role name you specified in the token request doesn't exist in the dataset. Double-check the role name in Power BI Desktop under Modeling > Manage Roles.

    `powerbi.embed()` returns undefined or doesn't render

    Check that the container element exists in the DOM when embed() is called. If you're calling it before the DOM is ready (e.g., in a script that runs before DOMContentLoaded), the function call will silently fail. Also verify that you're not calling embed() on the same container element twice — if the container already has an embedded component, call powerbi.reset(container) first.

    Filters throw "invalid filter" errors

    The most common cause is a mismatch between the filter target's table and column values and the actual names in the data model. The display name visible in the report may differ from the model name. Use the powerbi-client API to call embeddedReport.getFilters() on an existing filter to see the exact schema shape the API expects.

    Note: If you're embedding reports that use composite models — combining DirectQuery and Import mode data — be aware that some programmatic features, particularly exportData() on individual visuals, may behave differently or be unavailable for DirectQuery visuals. The limitations are documented in the Power BI composite models documentation, and understanding how storage modes interact will help you anticipate them.

    Token expiry handling fails silently

    If the tokenExpired event fires but your refresh doesn't work, check that your fetchEmbedConfig() call is actually completing. If your server's AAD token has expired, the server will fail to generate a new embed token. Add logging to your server's token generation endpoint to verify it's being called and succeeding.


    Security Checklist

    Before going to production with an embedded analytics implementation, verify every item on this list:

    • Client secret stored in environment variables or a secrets manager (Azure Key Vault, AWS Secrets Manager), never in source code
    • /api/embed-config endpoint protected by your application's authentication middleware
    • Effective identity constructed server-side from verified session data, with no user-controllable inputs influencing role assignment
    • No AAD access tokens or service principal credentials sent to the browser
    • RLS roles tested with multiple simulated identities to verify isolation
    • Token refresh implemented with tokenExpired event handler
    • Error states handled gracefully with user-facing messages (not raw API error strings)
    • Server-side logging of token generation failures for incident response

    Summary & Next Steps

    You've built a complete foundation for production-grade embedded analytics. The key architectural principles to internalize:

    • The service principal is your application's identity in Power BI's permission model. It never touches the browser.
    • The embed token is a short-lived, scoped credential that does reach the browser, and it's the only thing the powerbi-client SDK needs.
    • Effective Identity is the mechanism that bridges your application's user model to Power BI's RLS system. It's powerful and it must be implemented server-side.
    • The JavaScript API gives you two-way integration: your app can control the report, and the report's events can drive your app's behavior.

    Where to go from here:

    The reports you embed are only as good as their underlying design. The visual design patterns in Building Interactive Visuals: Advanced Charts, Maps, and Custom Formatting in Power BI apply directly to embedded contexts. Governance at scale — tracking who is viewing which embedded reports and how often — is covered in Mastering Power BI Usage Metrics and Audit Logs: Tracking Report Adoption, User Activity, and Governance at Scale. And when your embedded reports are ready for promotion from development to production, the deployment pipelines workflow automates that process in a way that's compatible with the workspace-based permission model we've built here.

    Embedded analytics is one of those capabilities that, once you've shipped it correctly, becomes a genuine competitive differentiator. The combination of Power BI's modeling and visualization power with your application's context, identity, and UX is something your users will actually use — which is the whole point.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Getting Started with Power BI

    Previous

    Mastering Power BI Tooltip Pages: Building Custom Visual Tooltips with DAX Measures and Drillthrough Context for Richer Report Interactivity

    Related Insights

    Power BIPractitioner

    Implementing Power BI Datamart with Automatic Schema Detection and Built-In SQL Endpoint to Deliver Self-Service Analytics Without a Dedicated Data Warehouse

    24 min
    Power BIPractitioner

    DAX What-If Analysis with Calculation Groups and Field Parameters: Modeling Price, Volume, and Mix Variance Scenarios Dynamically

    20 min
    Power BIPractitioner

    Mastering Power BI Tooltip Pages: Building Custom Visual Tooltips with DAX Measures and Drillthrough Context for Richer Report Interactivity

    22 min

    On this page

    • Introduction
    • Prerequisites
    • The Architecture You Need to Understand First
    • Step 1: Registering a Service Principal in Microsoft Entra ID
    • Step 2: Adding the Service Principal to the Power BI Workspace
    • Step 3: Generating the Embed Token Server-Side
    • Step 4: Embedding Reports with the JavaScript API
    • Step 5: Controlling Reports Programmatically
    • Applying Filters
    • Page Navigation
    • Bookmark Interactions
    • Visual-Level Interactions
    • Step 6: Implementing Effective Identity for Multi-Tenant Security
    • Step 7: Token Refresh and Production Considerations
    • Performance Optimization
    • Step 8: Advanced Embedding Patterns
    • Embedding Multiple Reports with a Single Token
    • Embedding a Single Visual
    • Responding to Cross-Filter Events
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • "403 Forbidden" when generating the embed token
    • Report loads but shows "access denied" or blank
    • RLS not being enforced (users see all data)
    • "The effective identity doesn't correspond to any RLS roles"
    • `powerbi.embed()` returns undefined or doesn't render
    • Filters throw "invalid filter" errors
    • Token expiry handling fails silently
    • Security Checklist
    • Summary & Next Steps