> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsolve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from v1 to v2

> Move an integration built on the UpsolveDashboard React component and the v1 dashboard API to the v2 application iframe and the v2 dashboard API.

This guide is for developers whose app embeds Upsolve through the
`<UpsolveDashboard />` React component and manages dashboards through the v1
[Dashboard Management API](/embedded-bi/dashboards/programmatic-management)
(`/v1/api/dashboard/*`). It shows how to move that integration to v2 **without
changing how your app works**:

* the component becomes an iframe,
* the v1 dashboard endpoints become their v2 equivalents,
* and the IDs you already hold keep working.

<Info>
  Your Upsolve team runs the data migration for your organization. It turns each v1
  **workspace** into a v2 **application**, and every dashboard you created, including
  those created through `/v1/api/dashboard/create`, into a dashboard of that
  application. Nothing on your side has to happen at the same moment: you can
  switch the iframe first and the API calls later.
</Info>

## How v1 concepts map to v2

| v1                                                     | v2                                                                                                                                                     |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Workspace                                              | **Application**. One per migrated workspace; `legacyWorkspaceId` on [List Applications](/api-reference/v2/endpoint/list-applications) tells you which. |
| Global dashboard (made in the Hub, or with an API key) | **Shared dashboard** (`kind: "template"`). Every user of the application sees it.                                                                      |
| Tenant's dashboard (made with a tenant JWT)            | **Dashboard in that user's space** (`kind: "dashboard"`). Only their project organization sees it.                                                     |
| Dashboard made with a project-user JWT                 | **Dashboard in that user's space**, owned by them.                                                                                                     |
| Tenant JWT (`register-tenant`)                         | **Project-user JWT**. After migration, `register-tenant` returns one, so your backend call does not change.                                            |
| `<UpsolveDashboard dashboardId=… />`                   | `<iframe src=".../share/application/{applicationId}?dashboardId=…&jwt=…" />`                                                                           |

When a user edits a shared dashboard, v2 gives them their own copy of it, and that
copy replaces the shared one in their list. The list endpoint and the iframe
both apply this rule, so a user always sees one entry per dashboard.

## What happens to your IDs

The migration records which v2 dashboard every v1 dashboard became. Because of
that:

* The **iframe accepts v1 dashboard IDs** as well as v2 ones.
* [List Application Dashboards](/api-reference/v2/endpoint/list-application-dashboards)
  returns `legacyDashboardId` next to each dashboard's v2 `id`.
* [List Applications](/api-reference/v2/endpoint/list-applications) returns
  `legacyWorkspaceId` next to each application's `applicationId`.
* [Look Up Dashboard](/api-reference/v2/endpoint/get-dashboard-location) turns
  any dashboard ID, v1 or v2, into `{ applicationId, dashboardId }`.

If your app discovers dashboards at runtime with `list-workspaces` →
`list-workspace-dashboards`, you have nothing to rewrite. Point those calls at
the v2 endpoints (step 3) and use the IDs they return. If you stored IDs, they
keep working in the iframe as they are.

## Step 1: Tokens (usually no change)

Keep calling `POST /v1/api/tenant/register-tenant` from your backend. After your organization's migration is finalized, it returns a
**project-user JWT** for the same end user instead of a tenant JWT. Pass that
token to the iframe as `jwt` and to the v2 API as `Authorization: Bearer <token>`.

If you are starting fresh, or want to move off `register-tenant`, use the v2
token flow:

1. [Register Organization](/api-reference/endpoint/register-project-organization)
2. [Register User](/api-reference/endpoint/register-project-user)
3. [Get Project User Token](/api-reference/endpoint/get-project-user-token)

See [Backend Setup](/embedded-bi/deploy-dashboards/backend-setup) for the full flow.

<Note>
  A tenant JWT issued before the migration is still accepted by the v2 API. It is
  translated to the project user the migration created for that tenant. The
  iframe needs a project-user JWT.
</Note>

## Step 2: Replace `<UpsolveDashboard />` with an iframe

### Before

```tsx theme={null}
import { UpsolveDashboard } from "@upsolve-labs/sdk";

<UpsolveDashboard
  dashboardId={dashboardId}
  tenantJWT={tenantJwt}
  hideHeader
  controlledFilterValues={{ region: ["APAC"] }}
/>;
```

### After: you only have the dashboard ID

`/share/dashboard/{dashboardId}` looks up the dashboard's application for you
and opens it. It accepts the v1 ID you already pass to `<UpsolveDashboard />`.

```tsx theme={null}
const HUB = "https://ai-hub.upsolve.ai";

function Dashboard({ dashboardId, jwt }: { dashboardId: string; jwt: string }) {
  const params = new URLSearchParams({
    jwt,
    hideHeader: "true",
    filter_region: JSON.stringify(["APAC"]),
  });
  return (
    <iframe
      title="Analytics"
      src={`${HUB}/share/dashboard/${dashboardId}?${params}`}
      style={{ width: "100%", height: 800, border: 0 }}
    />
  );
}
```

### After: you also have the application ID (recommended)

Pointing at the application directly saves the lookup step:

```tsx theme={null}
src={`${HUB}/share/application/${applicationId}?dashboardId=${dashboardId}&${params}`}
```

With `dashboardId`, the embed shows **exactly that dashboard**, with no sidebar or
tabs, just as `<UpsolveDashboard />` did. Leave `dashboardId` off to embed the
whole application with its own navigation. A dashboard ID the token cannot see
renders **"Dashboard not found."** rather than silently opening a different
dashboard.

### Props → URL parameters

| `<UpsolveDashboard />` prop | iframe                                                                                                                                                                                      |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dashboardId`               | `dashboardId` query param, or the path of `/share/dashboard/{dashboardId}`                                                                                                                  |
| `tenantJWT`                 | `jwt`, a project-user token (see step 1)                                                                                                                                                    |
| `hideHeader`                | `hideHeader=true`                                                                                                                                                                           |
| `hideDownloadButton`        | `hideDownloadButton=true`                                                                                                                                                                   |
| `hideTabs`                  | Implied by `dashboardId`                                                                                                                                                                    |
| `controlledFilterValues`    | `filter_{controlKey}=<JSON>`. See [Controlled Filters](/embedded-bi/filters/controlled-filters).                                                                                            |
| `onFilterChange`            | Listen for `embed:params` over `postMessage`. See [Live Filter Sync](/embedded-bi/sharing-and-export/iframe-messaging).                                                                     |
| `dbAuthToken`               | `dbAuthToken`                                                                                                                                                                               |
| `theme` / `themeConfigs`    | Set the application's theme in the Hub (**Application → Theme**).                                                                                                                           |
| Component sizing            | Size the iframe, or add `autoHeight=true&parentOrigin=<your origin>` to have it report its content height. See [Auto-height](/embedded-bi/sharing-and-export/iframe-messaging#auto-height). |

<Warning>
  Changing the iframe `src` reloads it. Keep the `src` stable across renders (for
  example, memoize the token) so the dashboard doesn't reload unnecessarily. See
  [Frontend Setup](/embedded-bi/deploy-dashboards/frontend-setup).
</Warning>

## Step 3: Dynamic listing

If your app builds its own navigation from the v1 list endpoints, swap them one
for one.

### Before

```ts theme={null}
const API = "https://api.upsolve.ai";

// Server side, API key
const { data: { workspaces } } = await fetch(
  `${API}/v1/api/dashboard/list-workspaces`,
  { headers: { Authorization: `Bearer ${API_KEY}` } }
).then((r) => r.json());

// Per user, tenant JWT
const { data: { dashboards } } = await fetch(
  `${API}/v1/api/dashboard/list-workspace-dashboards?workspaceId=${workspaces[0].workspace_id}`,
  { headers: { Authorization: `Bearer ${tenantJwt}` } }
).then((r) => r.json());
```

### After

```ts theme={null}
// A project-user token only sees its own project's applications, so this can
// run per user as well as server side with an API key.
const { data: { applications } } = await fetch(
  `${API}/v2/api/applications`,
  { headers: { Authorization: `Bearer ${jwt}` } }
).then((r) => r.json());

const app = applications[0];
const { data: { dashboards } } = await fetch(
  `${API}/v2/api/applications/${app.applicationId}/dashboards`,
  { headers: { Authorization: `Bearer ${jwt}` } }
).then((r) => r.json());

// Embed any of them:
//   /share/application/${app.applicationId}?dashboardId=${dashboards[i].id}&jwt=…
```

| v1 field                                          | v2 field                                                           |
| ------------------------------------------------- | ------------------------------------------------------------------ |
| `workspaces[].workspace_id`                       | `applications[].applicationId` (the old ID is `legacyWorkspaceId`) |
| `workspaces[].dashboard_id`                       | `applications[].defaultDashboardId`                                |
| `workspaces[].dashboard_name`                     | `applications[].name`                                              |
| `dashboards[].id`                                 | `dashboards[].id` (the old ID is `legacyDashboardId`)              |
| `dashboards[].tenantId !== null` (the user's own) | `dashboards[].kind === "dashboard"`                                |
| `dashboards[].ownerProjectUserId`                 | `dashboards[].ownerProjectUserId`                                  |

## Step 4: Dashboard writes

| v1                                                                                     | v2                                                                                                                                                                           |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/api/dashboard/create` `{ workspaceId, name }`                                | [`POST /v2/api/applications/{applicationId}/dashboards`](/api-reference/v2/endpoint/create-application-dashboard) `{ name }`                                                 |
| `POST /v1/api/dashboard/clone` `{ sourceDashboardId, name }`                           | [`POST /v2/api/applications/{applicationId}/dashboards/{dashboardId}/clone`](/api-reference/v2/endpoint/clone-application-dashboard) `{ name }`                              |
| `POST /v1/api/dashboard/add-charts` `{ targetDashboardId, charts }`                    | [`POST /v2/api/applications/{applicationId}/dashboards/{dashboardId}/charts`](/api-reference/v2/endpoint/add-application-dashboard-charts) `{ charts }`                      |
| `POST /v1/api/dashboard/remove-charts` `{ dashboardId, chartIds }`                     | [`DELETE /v2/api/applications/{applicationId}/dashboards/{dashboardId}/charts/{chartId}`](/api-reference/v2/endpoint/remove-application-dashboard-chart), one call per chart |
| `POST /v1/api/dashboard/append-charts-from` `{ sourceDashboardId, targetDashboardId }` | [`POST /v2/api/applications/{applicationId}/dashboards/{dashboardId}/append-charts`](/api-reference/v2/endpoint/append-application-dashboard-charts) `{ sourceDashboardId }` |
| `POST /v1/api/chart/save-from-chat`                                                    | Unchanged. Pass the returned `chartId` to the v2 add-charts endpoint.                                                                                                        |

Who you authenticate as decides where the dashboard lives, just as in v1:

* **API key**: the dashboard is **shared** with every user of the application.
  This is the v1 "global dashboard".
* **Project-user token**: the dashboard is created in **that user's space**. This
  is the v1 "tenant dashboard".

One rule is new. A project-user token cannot edit a shared dashboard, because
the change would reach every other user. Such a call answers
`409 SHARED_DASHBOARD_READ_ONLY`. Clone the dashboard first and edit the copy;
the copy lands in the user's own space.

<Note>
  With a user-scoped API key, pass `organizationId`: in the query string on `GET`
  and `DELETE`, and in the JSON body on `POST`.
</Note>

## What v1 does once your migration is finalized

| v1 endpoint                                                                            | Before finalize | After finalize                                                                                         |
| -------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------ |
| `GET /dashboard/list-workspaces`                                                       | v1 workspaces   | Your **applications**, in v1's response shape                                                          |
| `GET /dashboard/list-workspace-dashboards`                                             | v1 dashboards   | The application's dashboards in v1's shape, with v1 IDs wherever one exists                            |
| `POST /dashboard/create`, `clone`, `add-charts`, `remove-charts`, `append-charts-from` | Works           | **`410 MIGRATED_TO_V2`**, naming the v2 endpoint to use                                                |
| `<UpsolveDashboard />`                                                                 | Works           | Shows the v1 dashboard as it was at migration. It no longer receives changes, so switch to the iframe. |

The writes stop because a dashboard written through v1 after the migration would
land where v2 never reads it: it would return an ID and then never appear in any
embed. The lists keep answering so that an integration that only lists and
embeds keeps working throughout.

**Recommended order:**

1. Swap the component for the iframe (step 2).
2. Point the list calls at v2 (step 3).
3. Point the write calls at v2 (step 4).

The first two steps work before and after finalize. The third is required by
finalize.

## Troubleshooting

| Symptom                                      | Cause                                                                                                                                                                                                                   |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Iframe shows **"Dashboard not found."**      | The ID does not exist, belongs to another application, or is not visible to this user's project organization. Call [Look Up Dashboard](/api-reference/v2/endpoint/get-dashboard-location) with the same token to check. |
| Iframe shows **"Invalid or missing token."** | `/share/dashboard/{id}` needs a `jwt`: there is no anonymous session without an application.                                                                                                                            |
| `401 TENANT_NOT_MIGRATED`                    | A tenant JWT for a tenant the migration has not mapped yet. Request a new token from `register-tenant`.                                                                                                                 |
| `403 PROJECT_USER_WITHOUT_ORGANIZATION`      | The project user was registered without a project organization, so it has no space to hold dashboards.                                                                                                                  |
| `409 SHARED_DASHBOARD_READ_ONLY`             | A project-user token tried to edit a shared dashboard. Clone it first.                                                                                                                                                  |
| `410 MIGRATED_TO_V2`                         | A v1 dashboard write after finalize. Use the endpoint named in the error.                                                                                                                                               |
