# Microsoft Power BI

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Microsoft Power BI by embedding reports via a Custom Widget WebView. A Firebase Cloud Function authenticates with Azure AD using client credentials, calls the Power BI REST API to generate a short-lived embed token (valid ~60 minutes), and returns the token and embed URL to FlutterFlow. The Azure client secret never touches Dart — only the Cloud Function holds it.

## Embed Power BI Reports in Your FlutterFlow App

Microsoft Power BI is a leading enterprise BI platform with tight Azure integration. To embed a Power BI report in a FlutterFlow app, you use the 'embed for your customers' (app owns data) pattern: your backend authenticates to Azure AD with client credentials, requests a report-specific embed token from the Power BI REST API, and hands only that short-lived token to the Flutter client. The Flutter WebView loads the report using the embed token — no Azure credentials ever reach the device.

FlutterFlow compiles to a Flutter app running on the user's device, which means any secret stored in your project ships inside the compiled binary. The Azure AD client_secret is a full credential for your Azure application; if it leaked, an attacker could generate unlimited embed tokens or access other Azure resources your app is permitted to reach. This is why a Firebase Cloud Function (or Supabase Edge Function) is mandatory as the token broker. The Cloud Function holds the tenant_id, client_id, and client_secret; the FlutterFlow app only receives a {embedToken, embedUrl, reportId} bundle and renders it.

The most critical operational detail of this integration is the 60-minute embed token expiry. Power BI embed tokens are not renewable — you must generate a new one before the old one expires, or the report goes blank. Power BI Pro per-user licensing or Power BI Embedded capacity (A-SKU or EM-SKU) is required for the 'embed for customers' flow; check current pricing at powerbi.microsoft.com/pricing. Without the correct licensing or capacity, the GenerateToken API call will return 401 or 403 errors regardless of how correctly you've configured Azure AD.

## Before you start

- A Microsoft Azure account with permission to register an Azure AD application in your tenant
- A Power BI workspace with at least one published report, plus Power BI Pro or Power BI Embedded capacity (A/EM SKU) to enable the GenerateToken API
- A Firebase project with Cloud Functions enabled (Blaze pay-as-you-go plan required for outbound HTTP calls from functions)
- A FlutterFlow project on any paid plan (Custom Code is available on all paid tiers)
- The Power BI report ID and workspace (group) ID — visible in the report's URL in the Power BI web interface

## Step-by-step guide

### 1. Register an Azure AD app and grant Power BI API permissions

The first step happens entirely in the Azure Portal (portal.azure.com) and has nothing to do with FlutterFlow yet. Log in to portal.azure.com, navigate to Azure Active Directory → App registrations → New registration. Give the app a name (for example 'FlutterFlow Power BI Embed'), choose 'Accounts in this organizational directory only' for the supported account type, leave the redirect URI blank (this is a service app, not a user-facing one), and click Register. On the app's Overview page, copy three values: the Application (client) ID, the Directory (tenant) ID, and note that you are in a single-tenant setup. Next, navigate to Certificates & secrets → New client secret. Set a sensible expiry (12 or 24 months) and copy the secret Value immediately — Azure only shows this once. Now navigate to API permissions → Add a permission → Power BI Service → Delegated permissions and add: Report.ReadAll, Dataset.ReadAll, and Workspace.ReadAll (or the minimum permissions your use case requires). Click Grant admin consent for your directory. Finally, go to the Power BI Admin portal (app.powerbi.com/admin-portal → Tenant settings → Developer settings → Allow service principals to use Power BI APIs) and ensure it is enabled. If your Power BI workspace is in a Premium or Embedded capacity, also ensure the service principal has access to that workspace.

**Expected result:** You have three values: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET. The app appears in your Azure AD App registrations with Power BI API permissions and admin consent granted.

### 2. Write a Firebase Cloud Function that mints Power BI embed tokens

This Cloud Function is the secure token broker. It performs two HTTPS calls: first to Azure AD's token endpoint to exchange your client credentials for an OAuth token, then to the Power BI REST API's GenerateToken endpoint to get a report-specific embed token. The Cloud Function receives the report ID and workspace ID from FlutterFlow, makes both calls server-side, and returns {embedToken, embedUrl, reportId} to the app. Store your Azure credentials in Firebase Function config using the Firebase CLI: run firebase functions:config:set azure.tenant_id=VALUE azure.client_id=VALUE azure.client_secret=VALUE powerbi.workspace_id=VALUE. In FlutterFlow's Custom Code panel, navigate to Cloud Functions and author the function there, or deploy it from the Firebase Console. The function should be an HTTPS Callable to allow Firebase's built-in authentication check (context.auth) — only authenticated FlutterFlow users can request embed tokens. The embed token expiry is approximately 60 minutes; include the expiry timestamp in the response so the Flutter widget can schedule a refresh. Redeploy any time you rotate Azure credentials (which you should do on a schedule).

```
// Firebase Cloud Function (Node.js 20) — index.js
const functions = require('firebase-functions');
const fetch = require('node-fetch'); // or use built-in fetch in Node 20

exports.getPowerBIEmbedToken = functions.https.onCall(async (data, context) => {
  // Only authenticated FlutterFlow users can call this
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be signed in');
  }

  const tenantId = functions.config().azure.tenant_id;
  const clientId = functions.config().azure.client_id;
  const clientSecret = functions.config().azure.client_secret;
  const workspaceId = data.workspaceId || functions.config().powerbi.workspace_id;
  const reportId = data.reportId;

  if (!reportId) {
    throw new functions.https.HttpsError('invalid-argument', 'reportId is required');
  }

  // Step 1: Get Azure AD access token
  const aadTokenRes = await fetch(
    `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: clientId,
        client_secret: clientSecret,
        scope: 'https://analysis.windows.net/powerbi/api/.default'
      })
    }
  );
  const { access_token } = await aadTokenRes.json();

  if (!access_token) {
    throw new functions.https.HttpsError('internal', 'Failed to get Azure AD token');
  }

  // Step 2: Get Power BI embed token
  const embedTokenRes = await fetch(
    `https://api.powerbi.com/v1.0/myorg/groups/${workspaceId}/reports/${reportId}/GenerateToken`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${access_token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ accessLevel: 'View' })
    }
  );
  const embedData = await embedTokenRes.json();

  if (!embedData.token) {
    throw new functions.https.HttpsError('internal',
      `GenerateToken failed: ${JSON.stringify(embedData)}`);
  }

  const embedUrl =
    `https://app.powerbi.com/reportEmbed?reportId=${reportId}&groupId=${workspaceId}`;

  return {
    embedToken: embedData.token,
    embedUrl,
    reportId,
    // expiry ~60 min from now; subtract 5 min for safety
    expiresAt: Date.now() + (55 * 60 * 1000)
  };
});
```

**Expected result:** The Cloud Function deploys successfully. Calling it with a valid reportId and workspaceId returns {embedToken, embedUrl, reportId, expiresAt}. The Azure credentials are never exposed in the response.

### 3. Create a FlutterFlow API Call to request embed tokens from your Cloud Function

With the Cloud Function deployed, wire it into FlutterFlow so the app can fetch a fresh embed token when the user opens the Power BI screen. In FlutterFlow, click API Calls in the left nav → + Add → Create API Group. Name the group 'PowerBI' and set the base URL to your Firebase project's Cloud Functions regional base URL (visible in the Firebase Console under Functions → your function → copy the URL up to the function name). Add a single API Call inside the group named 'Get Embed Token'. Set the method to POST. Switch to the Body tab and set the JSON body: {"data": {"reportId": "{{reportId}}", "workspaceId": "{{workspaceId}}"}} — the 'data' wrapper is required for Firebase Callable Functions. In the Variables tab, add reportId (String, required) and workspaceId (String, optional — you can default it in the Cloud Function config). In the Headers tab, set Content-Type to application/json. Click Response & Test, paste a sample response {"result": {"embedToken": "eyJ...", "embedUrl": "https://app.powerbi.com/reportEmbed?...", "reportId": "abc-123", "expiresAt": 1720003600000}}, and click Generate JSON Paths. The paths $.result.embedToken, $.result.embedUrl, and $.result.expiresAt will be used to bind data to your Custom Widget. Save the API Call. You will call this from an On Page Load action on the Power BI screen.

```
{
  "method": "POST",
  "endpoint": "/getPowerBIEmbedToken",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "data": {
      "reportId": "{{reportId}}",
      "workspaceId": "{{workspaceId}}"
    }
  },
  "response_json_paths": {
    "embedToken": "$.result.embedToken",
    "embedUrl": "$.result.embedUrl",
    "expiresAt": "$.result.expiresAt"
  }
}
```

**Expected result:** The 'Get Embed Token' API Call appears in FlutterFlow with three JSON Paths. The Test button returns a valid embed token object when given a real reportId from your Power BI workspace.

### 4. Build a Custom Widget WebView that renders the Power BI report

Power BI reports embedded via the 'embed for customers' flow require the Power BI JavaScript SDK or a direct embed URL with the token passed as a query parameter. The simplest approach for FlutterFlow — and the one that works reliably on both iOS and Android — is a Custom Widget using flutter_inappwebview that loads the Power BI embed URL with the token injected. In the left nav, click Custom Code → + Add → Widget. Name it 'PowerBIReport'. In the Pubspec Dependencies field, add flutter_inappwebview with version ^6.0.0 (check pub.dev for the latest stable). Define three parameters: embedUrl (String, required), embedToken (String, required), and height (double, defaults to 600.0). The widget constructs the final URL by appending the embed token and sets it in an InAppWebView. It also sets up a timer to call back to FlutterFlow when the token is about to expire, triggering a re-fetch. Add the widget to your Power BI screen by dragging it from the Component palette. Bind its embedUrl and embedToken parameters to the output of the 'Get Embed Token' API Call run in On Page Load. Store the expiresAt value in a Page State variable, and add a timer action that fires when the current time approaches expiresAt to call the API again and update the widget's parameters — this prevents the report from going blank mid-session.

```
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class PowerBIReport extends StatefulWidget {
  const PowerBIReport({
    super.key,
    required this.embedUrl,
    required this.embedToken,
    this.height = 600.0,
  });

  final String embedUrl;
  final String embedToken;
  final double height;

  @override
  State<PowerBIReport> createState() => _PowerBIReportState();
}

class _PowerBIReportState extends State<PowerBIReport> {
  InAppWebViewController? _webViewController;

  String get _fullEmbedUrl {
    if (widget.embedUrl.isEmpty || widget.embedToken.isEmpty) return '';
    // Append the embed token as a query param for the JavaScript SDK
    final uri = Uri.parse(widget.embedUrl);
    return uri
        .replace(queryParameters: {
          ...uri.queryParameters,
          'autoAuth': 'false',
          'ctid': '', // optional: your tenant ID for multi-tenant
        })
        .toString();
  }

  @override
  Widget build(BuildContext context) {
    if (widget.embedUrl.isEmpty || widget.embedToken.isEmpty) {
      return SizedBox(
        height: widget.height,
        child: const Center(child: CircularProgressIndicator()),
      );
    }
    return SizedBox(
      height: widget.height,
      child: InAppWebView(
        initialUrlRequest: URLRequest(url: WebUri(_fullEmbedUrl)),
        initialSettings: InAppWebViewSettings(
          javaScriptEnabled: true,
          domStorageEnabled: true,
          allowsInlineMediaPlayback: true,
        ),
        onWebViewCreated: (controller) {
          _webViewController = controller;
        },
        onLoadStop: (controller, url) async {
          // Inject the embed token via JavaScript so Power BI's SDK picks it up
          await controller.evaluateJavascript(
            source: '''
              if (window.powerbi) {
                var report = powerbi.embed(document.querySelector("iframe"), {
                  type: "report",
                  embedUrl: "${widget.embedUrl}",
                  accessToken: "${widget.embedToken}",
                  tokenType: 0
                });
              }
            ''',
          );
        },
        onLoadError: (controller, url, code, message) {
          debugPrint('Power BI WebView error $code: $message');
        },
      ),
    );
  }
}
```

**Expected result:** The PowerBIReport widget appears on your canvas as a sized placeholder box. When run on a device with valid embedUrl and embedToken parameters, it loads and displays the Power BI report.

### 5. Implement the 60-minute embed token refresh to prevent blank reports

The embed token returned by Power BI's GenerateToken API expires after approximately 60 minutes. If a user leaves the Power BI screen open longer than that — common in enterprise apps where reports are left on screen during meetings — the report goes blank with no user-visible error message. You must implement a proactive token refresh. The recommended approach is to track the token expiry in FlutterFlow App State and use a Periodic Action or Timer. On the Power BI screen, add a Page State variable called embedTokenExpiresAt (Integer). When the On Page Load action runs the 'Get Embed Token' API Call, save the returned expiresAt value to this Page State variable. Then add a second action in the On Page Load flow: a Timer action set to fire 55 minutes after the current time (50-55 minutes is safe — 5 minutes before expiry). When the timer fires, trigger the 'Get Embed Token' API Call again, update embedToken, embedUrl, and embedTokenExpiresAt in Page State, and update the PowerBIReport widget's parameters by forcing a rebuild. You can trigger a widget rebuild by updating a boolean Page State variable that the widget's parent Container depends on. Alternatively, wrap the widget in a key that changes on token refresh, forcing Flutter to rebuild the WebView with the new token. This refresh loop keeps the report visible indefinitely as long as the user has the screen open.

**Expected result:** The Power BI screen automatically refreshes the embed token approximately every 55 minutes. Reports remain visible during long sessions without the user needing to navigate away and back.

### 6. Add the Power BI screen to your FlutterFlow app and test on device

Wire everything together on the Power BI screen in FlutterFlow. Create a new page named 'PowerBIDashboard'. Add an On Page Load action: Backend/API Call → PowerBI → Get Embed Token, set the reportId variable to a hard-coded report ID (or a Page Parameter if you want to show different reports on the same screen). From the Action output, set two Page State variables: currentEmbedToken (String) and currentEmbedUrl (String). Add the PowerBIReport Custom Widget to the page body, binding embedToken to the currentEmbedToken Page State and embedUrl to the currentEmbedUrl Page State. Set the height to fill the available screen height minus your navigation bar. Add a loading state: show a CircularProgressIndicator conditional on currentEmbedUrl being empty, and show the PowerBIReport widget when it is non-empty. Link to this screen from your app's navigation (tab bar, drawer, or button). To test, use FlutterFlow's Local Run feature — open your project, click Test → Local Run, and FlutterFlow will build and stream the app to a connected device or simulator. Navigate to the Power BI screen; you should see a loading spinner followed by the Power BI report. If you see a blank WebView, check the Cloud Function logs in Firebase Console for error details. If you'd prefer to have RapidDev handle the Azure AD setup, Cloud Function wiring, and token refresh loop for you, book a free scoping call at rapidevelopers.com/contact.

**Expected result:** The Power BI screen loads the report after a brief spinner. Navigating back and forth refreshes the token correctly. The report remains visible without going blank for the duration of a long user session.

## Best practices

- Never place the Azure client_secret, tenant_id, or client_id in FlutterFlow API Call headers or Dart custom action code — these are full Azure application credentials that belong only in Firebase Cloud Function environment config.
- Always implement the 55-minute token refresh cycle. Power BI embed tokens expire after approximately 60 minutes and cannot be renewed; without proactive refresh, the report goes blank mid-session with no user-facing error.
- Add the Azure AD service principal to the Power BI workspace as a member before testing the Cloud Function, or GenerateToken will return 403 even if Azure permissions look correct.
- Use flutter_inappwebview for the Custom Widget rather than FlutterFlow's built-in WebView — it provides the JavaScript execution environment, cookie handling, and error callbacks that Power BI's embed SDK needs to initialize correctly.
- Store the expiresAt timestamp from the Cloud Function response in FlutterFlow Page State so you can drive the refresh timer from a reliable, server-issued expiry value rather than guessing at a hard-coded interval.
- Rotate your Azure client secret before it expires (Azure shows the expiry date in Certificates & secrets). An expired secret silently breaks the Cloud Function's ability to mint new embed tokens, causing all Power BI screens to stop working.
- Test Power BI embedding using Local Run or a physical device — flutter_inappwebview Custom Widgets do not render in FlutterFlow's browser-based canvas or Run mode preview.
- For multi-tenant scenarios, pass the user's organization context from FlutterFlow App State to the Cloud Function so it can request row-level security filters in the GenerateToken call, scoping each user's view to their own data.

## Use cases

### Embedded analytics dashboard in a B2B SaaS mobile app

A FlutterFlow app built for B2B customers embeds a Power BI report showing each customer's key performance metrics. The Cloud Function generates a customer-scoped embed token, and the Custom Widget displays the report in a native WebView. Users can interact with Power BI's native slicers and drill-throughs without any additional FlutterFlow UI work.

Prompt example:

```
Add an Analytics tab to the app that loads a Power BI report filtered to the current customer's data, refreshing the embed token automatically every 50 minutes
```

### Executive KPI report viewer for a corporate app

A company-internal FlutterFlow app gives executives access to live Power BI dashboards on their phones. The app calls the Cloud Function on screen load, receives a fresh embed token, and renders the report in a full-screen WebView. Power BI's rich visualization library means the app delivers enterprise-grade charts without any custom charting code in Flutter.

Prompt example:

```
Build a 'My Dashboard' screen that embeds the Q3 Sales Performance Power BI report using a server-issued embed token that refreshes before the session expires
```

### Field team reporting app with role-based data views

A field operations FlutterFlow app embeds different Power BI reports depending on the user's role stored in the app's Firestore database. The Cloud Function receives the user's role and report ID from FlutterFlow, applies row-level security settings to the embed token, and returns the appropriate scoped report. Each field agent sees only their region's data.

Prompt example:

```
Show field agents their region's performance report and managers the full company report, embedding the correct Power BI report based on the logged-in user's role
```

## Troubleshooting

### Cloud Function returns a 403 or 'PowerBINotAuthorizedException' error from GenerateToken

Cause: The Azure AD service principal (your registered app) does not have access to the Power BI workspace containing the report, or the Power BI Embedded capacity is not configured correctly.

Solution: In the Power BI web interface (app.powerbi.com), open the workspace, click the three-dot menu → Workspace access, and add the Azure AD app's display name as a Member or Admin. Also verify in Azure that admin consent has been granted for the Power BI API permissions. If using Premium or Embedded capacity, ensure the workspace is assigned to that capacity.

### The embedded Power BI report goes blank after about an hour

Cause: The embed token has expired. Power BI embed tokens are valid for approximately 60 minutes and cannot be renewed — a new token must be generated before the old one expires.

Solution: Implement the proactive token refresh described in Step 5. Store the expiresAt timestamp returned by the Cloud Function, set a timer to fire 55 minutes after token issuance, and call the Cloud Function again at that point to update the widget's embedToken parameter.

### The Cloud Function call to Azure AD's token endpoint returns 401 or 'AADSTS' error

Cause: The tenant_id, client_id, or client_secret stored in Firebase Function config is incorrect, or the client secret has expired (Azure AD secrets have a maximum 24-month lifespan).

Solution: Log in to portal.azure.com, navigate to your app registration → Certificates & secrets, and check the secret expiry date. If expired, create a new secret and update your Firebase Function config with firebase functions:config:set azure.client_secret=NEW_VALUE, then redeploy. Also verify the tenant_id and client_id match the values on the app's Overview page.

### The Custom Widget shows a blank or white WebView on device (no Power BI content visible)

Cause: The embedToken or embedUrl parameters are empty when the widget first renders (the API Call hasn't completed yet), or the Power BI embed URL format is incorrect.

Solution: Add a loading guard: show a CircularProgressIndicator while embedUrl is empty, and only render the PowerBIReport widget after both embedToken and embedUrl are populated. Check the Cloud Function logs to confirm it is returning valid values. Also verify the embed URL format matches: https://app.powerbi.com/reportEmbed?reportId={reportId}&groupId={workspaceId}.

## Frequently asked questions

### Do I need Power BI Pro or Power BI Embedded to use the embed for customers flow?

Yes. The 'embed for your customers' (app owns data) flow that this guide uses requires either Power BI Embedded capacity (Azure A-SKU or EM-SKU assigned to your workspace) or Power BI Premium capacity. Standard Power BI Pro licenses do not support the GenerateToken API in the app-owns-data pattern. Check current capacity options and pricing at powerbi.microsoft.com/pricing or in the Azure Marketplace.

### Can I embed Power BI reports without a Firebase Cloud Function?

Not securely. Generating an embed token requires your Azure AD client_secret, which is a full credential for your Azure application. If you placed it in FlutterFlow's API Call headers or Dart code, it would ship inside the compiled APK/IPA and could be extracted by anyone who installs the app. A server-side function (Firebase Cloud Function or Supabase Edge Function) is the only secure option for holding Azure credentials.

### Why does my Power BI report go blank after an hour?

Power BI embed tokens expire after approximately 60 minutes. Unlike OAuth refresh tokens, they cannot be renewed — you must generate a new one from the GenerateToken API. To prevent blank reports, implement a proactive refresh: store the expiresAt timestamp returned by the Cloud Function, set a Flutter timer to fire 55 minutes later, call the Cloud Function again, and update the Custom Widget's embedToken and embedUrl parameters. See Step 5 for the full implementation.

### Will the Power BI Custom Widget work on iOS, Android, and web?

The flutter_inappwebview Custom Widget works well on iOS and Android, rendering the Power BI report in a native WebView. For FlutterFlow's web builds, WebView-in-WebView behavior varies by browser and may encounter CORS or cross-origin iframe restrictions. Mobile builds (iOS/Android) do not enforce CORS and are the most reliable targets for this integration.

### How do I show different reports to different users in the same app?

Store the appropriate Power BI report ID (and optionally workspace ID) in the user's profile in Firestore or Supabase. When the Power BI screen loads, read the user's reportId from their profile and pass it as a parameter to the 'Get Embed Token' API Call. The Cloud Function uses that reportId in the GenerateToken request, returning an embed token scoped to the correct report for each user. You can also pass row-level security filters in the GenerateToken body to scope data within a single report by user.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/microsoft-power-bi
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/microsoft-power-bi
