Power BI Embedded

An interactive visual guide for BI professionals — how embedding works, row-level security, embed tokens, and multi-tenant isolation.

1 What Is Power BI Embedded?

Take your Power BI reports and display them inside your own web application. End users see full interactivity — slicers, filters, drill-through — without ever visiting app.powerbi.com.

📊 Same Reports

Build in Power BI Desktop, publish to a workspace — exactly the same workflow you know. Nothing changes in how you author.

🔑 No User Licenses

End users do not need Power BI Pro or PPU licenses. Your app's capacity handles everything.

🎨 Your Branding

Reports render inside your app's UI. You control the navigation, layout, and branding around the report.


2 Two Embedding Patterns

Choose based on who your audience is and whether they have Power BI licences.

🏢

App Owns Data

Recommended for customers

Your application authenticates with a service principal. End users have no Entra ID / Power BI interaction.

Who logs in?Your app (service principal)
User licence?Not needed
Best forExternal users, ISV apps, portals
👤

User Owns Data

For internal teams

Each user logs in to Entra ID themselves. They need their own Pro or PPU licence.

Who logs in?Each user via Entra ID
User licence?Pro or PPU required
Best forInternal dashboards, intranet apps

3 What You Need

Three layers — Power BI, Azure, and your web application.

Power BI

BI Side (what you know)

  • A Power BI workspace
  • Published reports & datasets
  • RLS roles (optional)
Azure

Azure Side (one-time setup)

  • Entra ID app registration → Client ID + Secret
  • Service principal added to workspace
  • Admin enables SP API access
App

Your Web App

  • Server: any language (Python, .NET, Node.js…)
  • Client: Power BI JS SDK
  • Config: Tenant ID, Client ID, Workspace ID, etc.

4 How It Works — Step by Step

Follow the journey from report publishing to a user seeing the embedded report.

1
Build Report
Power BI Desktop
2
Publish
To workspace
3
Copy IDs
Workspace + Report
4
User Visits App
Logs in
5
App → Entra ID
Gets access token
6
GET Report
Embed URL + metadata
7
GenerateToken
Scoped + RLS
8
Embed in Browser
JS SDK renders
9
User Interacts
Slicers, filters, drill

📍 Where Do the IDs Come From?

Open your report in the Power BI Service. The URL is:

https://app.powerbi.com/groups/WORKSPACE-ID/reports/REPORT-ID

Tenant ID and Client ID come from the Entra ID app registration (set up by IT/admin).


5 The Power BI REST APIs

Your app talks to Power BI through a set of REST APIs. Here are the key endpoints involved in embedding, what each one does, and how they chain together.

🔑 Authentication First

Before calling any Power BI API, your server must get an Entra ID access token by authenticating with your Tenant ID, Client ID, and Client Secret via the Microsoft Identity platform (https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token). This access token is sent as a Bearer header on every subsequent API call.

POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token grant_type = client_credentials client_id = {client-id} client_secret = {client-secret} scope = https://analysis.windows.net/powerbi/api/.default → Returns: { "access_token": "eyJ0eX..." }
1
GET List Reports in Workspace
GET /v1.0/myorg/groups/{workspace-id}/reports
Returns all reports in a workspace — their IDs, names, embed URLs, and dataset IDs. This is how your app discovers which reports are available.
Response (simplified):
{ "value": [ { "id": "report-guid-1", "name": "Provider Performance", "embedUrl": "https://app.powerbi.com/reportEmbed?reportId=...", "datasetId": "dataset-guid-1" } ] }
2
GET Get Report
GET /v1.0/myorg/groups/{workspace-id}/reports/{report-id}
Fetches metadata for a single report. Returns the same fields as above but for one specific report. Your app uses this when it already knows which report to embed.
3
POST Generate Embed Token
POST /v1.0/myorg/GenerateToken
The most important call. Creates a short-lived embed token scoped to specific reports, datasets, and (optionally) an RLS identity. The browser uses this token to render the report.
Request body:
{ "datasets": [{ "id": "dataset-guid" }], "reports": [{ "id": "report-guid" }], "identities": [{ ← optional, for RLS "username": "alice@contoso.co.uk", "roles": ["DynamicRLS"], "datasets": ["dataset-guid"] }] }
Response:
{ "token": "eyJ0eXAiOiJKV1Qi...", "tokenId": "guid", "expiration": "2026-03-06T15:00:00Z" }
4
JS SDK Embed in Browser
powerbi.embed(element, config)
Not a REST call — this is client-side. Your server passes the embed URL (from step 1/2) and the embed token (from step 3) to the browser. The Power BI JS SDK takes over and renders the report in an iframe.
JavaScript:
const config = { type: "report", id: "report-guid", embedUrl: "https://app.powerbi.com/reportEmbed?...", accessToken: "eyJ0eXAi...", tokenType: models.TokenType.Embed, settings: { panes: { filters: { visible: false } } } }; powerbi.embed(reportContainer, config);

Other Useful Endpoints

GET List Workspaces
GET /v1.0/myorg/groups

Lists all workspaces the service principal has access to. Useful for multi-tenant setups where each client has their own workspace.

GET Get Dataset
GET /v1.0/myorg/groups/{workspace-id}/datasets/{dataset-id}

Returns dataset metadata including whether RLS is configured. Helps your app decide whether to include identities in the token request.

POST Refresh Dataset
POST /v1.0/myorg/groups/{workspace-id}/datasets/{dataset-id}/refreshes

Triggers a data refresh. You can automate this to keep embedded reports up to date without logging in to the Power BI Service.

GET Get Pages
GET /v1.0/myorg/groups/{workspace-id}/reports/{report-id}/pages

Lists all pages in a report. Useful if your app needs to show a page picker or navigate programmatically to a specific page.

📡 Base URL & Versioning

All Power BI REST API calls go to: https://api.powerbi.com/v1.0/myorg/
The v1.0 version is stable and used in production. Microsoft publishes updates via the REST API reference docs.


6 Row-Level Security (RLS)

Control which rows each user sees — same DAX roles as always, but your app assigns the identity instead of Entra ID.

RegionProviderRevenue
EastAlpha Care£42,000
EastBeta Health£38,500
WestGamma Med£51,200
WestDelta Labs£29,800
NorthEpsilon Care£33,100

Static RLS

The DAX filter is hardcoded in the role definition. The username passed in the token is ignored.

[Region] = "East"

Use when: you have a small, fixed number of data segments (regions, departments) and want a named role for each.

UserUPNProviderIDProviderRevenue
alice@contoso.co.ukP001Alpha Care£42,000
bob@contoso.co.ukP002Beta Health£38,500
carol@contoso.co.ukP003Gamma Med£51,200

Dynamic RLS

One role, many users. The username in the token replaces USERPRINCIPALNAME() at query time.

[UserUPN] = USERPRINCIPALNAME()

Recommended. Define once — the identity value does the filtering. No new roles needed when you add users.

UserUPNProviderIDProviderRevenue
alice@contoso.co.ukP001Alpha Care£42,000
bob@contoso.co.ukP002Beta Health£38,500
carol@contoso.co.ukP003Gamma Med£51,200
dan@contoso.co.ukP004Delta Labs£29,800

The Admin Problem

Once any RLS role exists on a dataset, every token request must include an EffectiveIdentity. There's no "skip RLS" option.

Solution: Create an AllAccess role with an empty DAX filter (no restriction). Pass it for admin users.

Role: AllAccess DAX filter: (none) → All rows visible
ProblemCauseFix
400 "requires effective identity" Dataset has RLS roles but no identity was passed Always send EffectiveIdentity when RLS is defined
400 "shouldn't have effective identity" Dataset has no RLS roles but you passed an identity Remove the identities field from the token request
User sees no data username doesn't match any rows Check exact value — case-sensitive, must match data
User sees all data Role has no filter / broken relationships Verify relationship chain from security table to facts

🔐 What your app sends to the API

{ "datasets": [{ "id": "<dataset-id>" }], "reports": [{ "id": "<report-id>" }], "identities": [{ "username": "alice.smith@contoso.co.uk", "roles": ["DynamicRLS"], "datasets": ["<dataset-id>"] }] }

7 How Embed Tokens Work

A temporary, scoped key that grants a browser access to a specific Power BI report. The browser cannot escalate or modify it.

🎫 What's Inside an Embed Token

When your server calls POST /GenerateToken, Power BI returns a JWT that encodes:

── HEADER ────────────── algorithm: RS256 type: JWT ── PAYLOAD ───────────── report_id: abcd-1234… dataset_id: efgh-5678… workspace_id: ijkl-9012… rls_identity: alice@contoso… rls_roles: ["DynamicRLS"] expires: 2026-03-06T15:00Z ── SIGNATURE ─────────── signed by Power BI (cannot be forged)

🔄 Token Lifecycle

Server authenticates with Entra ID using Client ID + Secret → receives an Entra ID access token
Server calls POST /GenerateToken with the report, dataset, and optional RLS identity
Power BI returns a scoped embed token (JWT) valid for ~1 hour
Server sends embed token + embed URL to the browser
JS SDK embeds the report in an iframe using the token
Before expiry, fetch a new token and call report.setAccessToken() for seamless refresh

✅ Security Guarantees

  • Token never contains your Client Secret
  • Token is signed by Power BI — cannot be forged
  • RLS identity is baked in at generation — browser can't change it
  • Scoped to specific report(s) and dataset(s)

⚠️ Key Properties

  • Lifetime: ~1 hour default (must refresh)
  • Scope: Token for Report A can't access Report B
  • One use intent: Generate fresh per session/load
  • Disposable: Short-lived, limited blast radius

8 Workload Isolation — Keeping Clients Separate

When multiple clients use the same embedded app, you need to ensure data separation, performance isolation, and security.

Capacity
F4 (shared)
Workspace
Single workspace
📊 Report 🗄️ Dataset (all clients)
RLS filters per client
Data separation: Logical (DAX)
Performance isolation: None
Setup effort: Low
Cost: Lowest
Risk: Misconfigured RLS = data leak
Scalability: Limited by dataset size
Capacity
F4 (shared)
Client A
📊 🗄️
Client B
📊 🗄️
Client C
📊 🗄️
Data separation: Physical (separate datasets)
Performance isolation: Partial
Setup effort: Medium
Cost: Medium
Risk: Low — wrong workspace = no data
Scalability: Good
Capacity 1 — F4
Client A
📊🗄️
Client B
📊🗄️
Capacity 2 — F8
Client C
📊🗄️
Data separation: Physical
Performance isolation: Full
Setup effort: High
Cost: Highest
Risk: Lowest
Scalability: Best
Shared + RLS Separate Workspaces Separate Capacities
Data isolationLogical (DAX)PhysicalPhysical
Performance❌ Shared⚠️ Partial✅ Full
Cost✅ Lowest⚠️ Medium❌ Highest
CustomisationLimitedFullFull
Risk if misconfigured❌ Data leak⚠️ Low✅ Lowest
Management✅ Low⚠️ Medium❌ High

9 Capacity & Licensing

To embed reports for external users (App Owns Data), you need dedicated capacity.

Power BI Embedded (A SKU)

Pay-as-you-go · Azure resource
Best for: dev, testing, variable workloads

Pause/resume on demand. Scale up/down via Azure Portal. Billed per hour.

Fabric Capacity (F SKU)

Monthly commitment
Best for: production, steady workloads

Full Fabric features (Lakehouse, Notebooks, etc.) plus embedding. Fixed monthly cost.

💡 Development Tip

For development and testing, you can use a Pro licence with limited API calls — you don't need a paid capacity until you go to production.


📋 Quick Reference

ConceptWhat It Means for You
Publish reportsSame as always — Desktop → Publish → Workspace
App registrationOne-time Entra ID setup to give your app API access
Embed tokenShort-lived JWT generated server-side, scoped to a single report
JS SDKRenders the report in the browser — you don't build a viewer
RLSSame DAX roles as always, but the app passes the identity
No user licencesEnd users don't need Pro/PPU — the capacity handles it
Workload isolationChoose from shared RLS → separate workspaces → separate capacities