# Migrate to the new embed payload
> Move an existing per-dashboard embed link to the new embed payload step by step: shared credentials, objects, permissions, settings, filters, drill-through, and the new iframe URL
This guide is for teams that already embed dashboards with **per-dashboard embed links** (the setup documented in [Legacy single-dashboard embedding](/embedded/single-dashboard/)) and want to move to the **new embed payload**. It walks through each part of a legacy payload and shows where it goes in the new one.
Migrating is optional. Your existing embed links keep working, so you can move when it suits you, one dashboard at a time if you like.
## Benefits
The new embed payload works the same way as your legacy embed: your backend signs a JWT, and your frontend loads it in an iframe. Only the payload syntax and the credentials change.
Switching to the new embed payload gets you:
- **One embed credential**: a single Key ID and Secret for every embed, instead of an embed code and secret key per dashboard. It's easier to set up, and there's only one secret to store and manage.
- **Multiple dashboards in one embed**: list several dashboards in one payload and let viewers move between them
- **Exploration on your datasets**: drill-down, break down, view underlying data, and building charts on the fly with [self-serve exploration](/embedded/self-serve-exploration)
- **User-built dashboards**: personal and organization workspaces through [Identity & Workspace](/embedded/identity-workspace)
- **Ask AI**: conversational analytics with [Ask AI](/embedded/ask-ai)
- **Email subscriptions**: scheduled reports with [email subscriptions](/embedded/email-subscriptions)
## What changes at a glance
Most of your payload stays the same. The main changes are the credentials and how you tell Holistics which dashboards to embed.
## Prerequisites
Here's what to have ready before you begin:
- **Admin access in Holistics**, so you can generate embed credentials.
- **Access to your application code.** Your backend builds and signs the payload, and your frontend loads the embed, so you'll update both.
- **Dashboards as-code.** Make sure the dashboards you embed are [canvas dashboards](/docs/dashboards/) defined as code in AML. Only these can move to the new embed payload. Any others stay on their legacy embed links.
- **A local or staging environment to test in.** We recommend testing the new embed carefully there before you replace the legacy iframe for real users.
## Migration
For most embeds, only two things change: the credential you sign with, and how the payload says which dashboard to show.
### Step 1: Create and replace your embed credentials
The new embed payload uses one shared credential for every embed, instead of an embed code and secret key per dashboard. Create it once, then swap it into your code.
**1. Generate shared credentials.** Go to **Tools → Embedded Analytics**, click **Generate embed credentials**, and note the two values:
- **Key ID**: identifies your embeds in the iframe URL
- **Secret**: signs the payload. Keep it on your backend.
See [Quickstart, Step 1](/embedded/quickstart#step-1-enable-embedded-analytics-and-generate-credentials) for a walkthrough of this page. Generating shared credentials doesn't change your per-dashboard embed codes or secret keys, so your legacy embed links keep working.
**2. Replace them in your code.** Swap the old credentials for the new ones on both sides:
- **Backend:** sign the payload with the shared **Secret** instead of the per-dashboard secret key:
```javascript
// Before: jwt.sign(payload, secretKey, { algorithm: 'HS256' })
const token = jwt.sign(payload, embedSecret, { algorithm: 'HS256' });
```
- **Frontend:** point the iframe at your **Key ID** instead of the embed code, keeping the same regional domain (`secure`, `us`, or `eu`) or custom domain:
```html
```
### Step 2: List your dashboards in `objects.dashboards`
In the legacy setup, the embed code in the iframe URL decides which dashboard loads. In the new embed payload, the payload says which dashboards to show, by their **unique name in AML** (the name after `Dashboard`), not by numeric ID, embed code, or display title.
```javascript
const payload = {
objects: {
dashboards: ['sales_overview'], // unique AML name of the dashboard you embedded before
},
// your existing settings and permissions stay as they are
exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
};
```
Your dashboard now loads through the new embed payload. From here, you can [unlock more features](/embedded/quickstart#next-add-more-features-for-your-users) such as exploration, user-built dashboards, and Ask AI by adding keys to the same payload.
## What stays the same
The rest of your payload keeps working as it is, so there's nothing to rewrite:
- **`settings`**: same object, same keys. If yours still sets `enable_export_data`, you can drop it: it's deprecated. See the [Settings reference](/embedded/parameters-reference#settings) for everything you can set.
- **`permissions`**: row-level rules in `permissions.row_based` keep the same shape (`path`, `operator`, `values`, optional `modifier`).
:::warning Dataset row-level permissions now apply too
Row-level permissions defined in your datasets now apply to the embed automatically, combined with `permissions.row_based` using AND. Consider moving your rules into [dataset permissions matched by `user_attributes`](/embedded/row-level-permissions#option-b-attribute-based-permissions-in-datasets), or pass [`__ALL__`](/embedded/row-level-permissions#let-a-viewer-see-everything-with-__all__) for an attribute to bypass a dataset rule.
:::
- **`exp`**: unchanged. Keep it at 24 hours or less.
## Replicate drill-through and default filter values
Two legacy features work a little differently. Skip this section if you don't use them.
### Drill-through
Drill-through in an embed now works the same way as drill-through inside Holistics. Instead of a `drillthroughs` key in the payload, you set it up in two places:
1. **Set up drill-through on your dashboards in Holistics**, exactly as you would for internal users. See [Drill-through](/docs/interactions/drill-through).
2. **Add the destination dashboards to `objects.dashboards`**, by unique name. Viewers can only drill into dashboards that are part of the embed, so this list controls where they can go.
```javascript
const payload = {
objects: {
dashboards: [
'sales_overview', // starting point
'product_details', // was "123" in drillthroughs
'customer_analysis', // was "456" in drillthroughs
],
},
exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
};
```
Legacy `drillthroughs` could also override filters on each destination dashboard. That isn't supported, so use the [`dashboard:filters:apply`](/embedded/embed-events#dashboardfiltersapply) event after the destination loads instead.
### Default filter values
The new embed payload has no `filters` key, so the `default_condition` entries from your legacy payload move out of the payload.
**For simple conditions, use URL query parameters.** Add each filter's unique name to the embed URL with the value to apply. Query parameters support `is`, `equal to`, and `matches`-style conditions, and apply to the dashboard the URL opens to:
```
https://{region_domain}.holistics.io/embed/{embed_key_id}/objects/sales_overview?_token=...®ion=Europe
```
**For other conditions, use embed events.** URL query parameters don't support relative dates (such as "last 6 months") or other custom operators. For those, change the dashboard's filters with the [`dashboard:filters:apply`](/embedded/embed-events#dashboardfiltersapply) event once the dashboard has loaded:
```javascript
iframe.contentWindow.postMessage(
{
eventName: 'dashboard:filters:apply',
payload: {
filters: {
region: { operator: 'is', values: ['Europe'] },
},
},
},
'https://your-holistics-domain.com'
);
```
See [Default filter values](/embedded/parameters-reference#default-filter-values) for the URL syntax and [Sync filters between your app and the dashboard](/embedded/embed-events#sync-filters-between-your-app-and-the-dashboard) for the event pattern.
:::danger Filters were never a security feature
URL parameters aren't signed, and viewers could always change legacy default filters in the dashboard anyway. If a legacy `filters` entry was quietly acting as a data restriction, move it to `permissions.row_based` or `user_attributes` instead. See [Enforcing data access control the right way](/embedded/security#enforcing-data-access-control-the-right-way).
:::
## Before and after
Here's a complete legacy payload next to its new version. It embeds a sales dashboard with one row-level rule, auto-run on, a default region filter, and one drill-through destination. Highlighted lines are the ones that change: everything else stays as it is.
```javascript
const jwt = require('jsonwebtoken');
// highlight-start
const embedCode = '275XXXXXXXXX'; // per-dashboard embed code
const secretKey = '382fXXXXXXXX'; // per-dashboard secret key
// highlight-end
const payload = {
settings: {
dashboard_autorun_on_changes: true,
},
permissions: {
row_based: [
{
path: { dataset: 'ecommerce', model: 'countries', field: 'name' },
operator: 'is',
values: ['Vietnam'],
},
],
},
// highlight-start
filters: {
region: {
default_condition: { operator: 'is', values: ['Asia'], modifier: null },
},
},
drillthroughs: {
'123': {}, // product_details
},
// highlight-end
exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
};
// highlight-start
const token = jwt.sign(payload, secretKey, { algorithm: 'HS256' });
const url = `https://secure.holistics.io/embed/${embedCode}?_token=${token}`;
// highlight-end
```
```javascript
const jwt = require('jsonwebtoken');
// highlight-start
const embedKeyId = 'abc123XXXXXX'; // Step 1: shared Key ID
const embedSecret = '9f1dXXXXXXXX'; // Step 1: shared Secret
// highlight-end
const payload = {
// highlight-start
objects: {
dashboards: ['sales_overview', 'product_details'], // Step 2, plus the drill-through target
},
// highlight-end
settings: {
dashboard_autorun_on_changes: true,
},
permissions: {
row_based: [
{
path: { dataset: 'ecommerce', model: 'countries', field: 'name' },
operator: 'is',
values: ['Vietnam'],
},
],
},
exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
};
// highlight-start
const token = jwt.sign(payload, embedSecret, { algorithm: 'HS256' });
// Key ID replaces the embed code; the default filter moves to the URL
const url = `https://secure.holistics.io/embed/${embedKeyId}/objects/sales_overview?_token=${token}®ion=Asia`;
// highlight-end
```
## Recommended rollout plan
Legacy links keep working throughout, so there's no need to switch every customer at once. We recommend rolling out in phases:
1. **Build and test locally.** Migrate one embedded dashboard in your local environment and compare it side by side with the legacy embed. Load it as a few different viewers and confirm each one sees only their own rows.
2. **Verify in staging.** Deploy to staging and test with realistic viewer identities, including any drill-through and default filter behavior.
3. **Release to a small group.** Serve the new iframe to a subset of customers first, for example behind a feature flag, and keep the legacy iframe as a fallback.
4. **Roll out to everyone.** Once the new embed behaves as expected, switch the remaining customers.
5. **Retire the legacy code.** When no viewer uses the old iframe URL, remove the legacy payload code and embed links.
## FAQs
### Do my existing embed links stop working?
No. Per-dashboard embed links keep working, and the [legacy pages](/embedded/single-dashboard/) stay available so you can maintain them. Nothing forces you to migrate.
### Can I migrate one dashboard at a time?
Yes. Each legacy embed link is independent, so you can move one dashboard to the new embed payload while the others keep running on their own embed codes. Once several dashboards are migrated, you can also combine them into a single embed.
### Can I reuse my existing tokens?
Not as they are. A new embed expects a payload in the new shape, signed with the shared Secret. Your backend builds new tokens for the new iframe URL, while the old code path keeps serving legacy links until you retire it.
### Where do I find a dashboard's unique name?
Open the dashboard's code in Holistics and check the name after `Dashboard` in its AML definition. For example, `Dashboard sales_overview {` has the unique name `sales_overview`.
### Do I have to persist my setup as an Embed Portal?
No. Listing objects inline in the payload is enough. If your payloads grow or you want the list of embedded objects under version control, you can [persist your setup as code](/embedded/embed-portal/) later without changing credentials or the iframe.
## Related documentation
- [Quickstart](/embedded/quickstart): the new setup end to end
- [Embed payload reference](/embedded/parameters-reference): every payload and URL parameter
- [Row-level permissions](/embedded/row-level-permissions): ad hoc rules and dataset rules
- [Embed events](/embedded/embed-events): control filters and navigation from your app
- [Legacy single-dashboard embedding](/embedded/single-dashboard/): the setup you're migrating from