## Overview

Native Dashboard Apps have three separate integration contracts:

- The browser **V2 SDK** delivers capability-limited context, read-only events, safe frame commands,
  and optional signed identity for the app's own backend.
- The account **REST API** lets an administrator or custom role with `integration_manage` create,
  inspect, update, reorder and delete apps and
  their installations.
- The account **MCP server** exposes the same installation management contract as four optional tools
  in **Channels & integrations → Dashboard Apps**.

The SDK authenticates only a short-lived embedded-app identity; it does not expose a generic API
proxy. Business writes belong in your backend, authenticated to REST with a least-privilege token.

## Prerequisites

- `dashboard_apps_native_surfaces` enabled for the account.
- An HTTPS Dashboard App hosted on an origin different from the Conversa Labs dashboard for V2 mode.
- An account administrator, or custom role with `integration_manage`, and an `api_access_token` for REST management.
- For MCP, an account MCP profile whose acting user has that permission and whose selected modules
  include **Dashboard Apps**.
- CSP `frame-ancestors` configured to allow the exact Conversa Labs origin without a conflicting
  `X-Frame-Options` response header.

## Step by step

### 1. Connect the V2 browser SDK

Import the SDK from the stable, versioned path of your Conversa Labs deployment. It redirects to the
current fingerprinted build and exposes named JavaScript module exports.

```js
import { connect } from '/dashboard-app-sdk/v2.js';

try {
  // The host injects cl_* launch parameters, so origin and installation are auto-discovered.
  // Explicit options remain available for controlled tests.
  const client = await connect();

  const stop = await client.subscribe(
    ['context.initialized', 'conversation.changed', 'theme.changed'],
    event => {
      console.log(event.context_revision, event.data);
    }
  );

  await client.setHeight(520);
  document.querySelector('#documentation').addEventListener('click', () => {
    client.openLink('https://app.example.com/docs');
  });

  // Later: await stop(); client.disconnect();
} catch (error) {
  console.error(error.code);
}
```

Use the named module imports shown above. They are the stable integration contract for new apps. If
your app cannot consume named exports, the same file also exposes `window.ConversaLabsDashboardAppSDK`
with the same functions.

**Call `connect()` as soon as your page loads**, not behind authentication or a round trip to your own
backend. The host starts the handshake when the frame finishes loading and keeps retrying for up to 10
seconds; an app that only starts listening after that window gets `handshake_timeout`. A static
`import` at the top of a `<script type="module">` is the supported shape. A dynamic `import()` works as
long as it is awaited immediately — do not defer loading the SDK until after you render the screen.

`connect` reads `cl_dashboard_origin` and `cl_installation_id` from the launch URL, validates the exact
dashboard origin, negotiates protocol `2.0`, and resolves after the host handshake. The client exposes
`installationId`, `capabilities`, `connected`, `subscribe`, `unsubscribe`, `setHeight`, `openLink`,
`getIdentityAssertion` and `disconnect`.

Core events are `context.initialized`, `conversation.changed`, conversation status/assignment/labels,
message create/update, contact/current-user updates, locale/theme/permissions changes and
`installation.changed`. Conversation is omnichannel and works for email, WhatsApp, SMS and other
inboxes; sidebar context omits conversation and contact data. Always program to
the announced `capabilities`, optional fields and increasing `context_revision`; resynchronize rather
than applying stale data.

#### Secure identity for n8n and other backends

Do not send identity or credentials in the iframe query string. In `context.initialized`, use:

- `account.id` and `account.name` to identify the account;
- `current_user.id`, `current_user.name`, `current_user.role` and `current_user.avatar_url` to identify
  the person who opened the app;
- `installation.id`, `surface`, and sidebar-only `sidebar_category` and `sidebar_icon` to identify the
  installation, location, native category, and selected icon;
- on the conversation surface, `conversation`, `contact`, `permissions` and message events for the
  operational context.

The bridge never exposes user/session/CSRF, REST, MCP, or channel-provider credentials. When granted
per installation, `current_user:email` adds current-user email; `contact:email` and `contact:phone`
add contact data on the conversation surface. Without the capability, the field is omitted from
context and events.

To let an app backend or n8n webhook verify identity without trusting browser-provided IDs, request a
short-lived assertion:

```js
const identity = await client.getIdentityAssertion();
await fetch('https://app.example.com/api/dashboard-session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(identity),
});
```

The app backend POSTs `assertion` to the returned `introspection_url`, optionally with
`expected_origin: "https://app.example.com"`. An active response contains account, user,
installation, surface and capability claims. The assertion expires after two minutes and every
introspection revalidates feature, installation, active user, membership, audience and origin. The
public verifier rejects assertions above 16 KiB before signature work. It is **not** a
REST/MCP token and must not be stored or used as a bearer credential. Additional data and writes use
the backend's own least-privilege REST credential or restricted MCP profile.

### 2. Manage installations with REST

All paths are account-scoped. The feature-off response is `404 resource_not_found`; authenticated
members without management permission receive `403 forbidden` for management operations.

```bash
# List visible installations; administrators also receive audience details.
curl -sS "https://support.example.com/api/v1/accounts/1/dashboard_app_installations?surface=conversation" \
  -H "api_access_token: API_TOKEN"

# Create one installation. dashboard_app_id cannot be changed afterwards.
curl -sS -X POST "https://support.example.com/api/v1/accounts/1/dashboard_app_installations" \
  -H "api_access_token: API_TOKEN" -H "Content-Type: application/json" \
  --data '{"dashboard_app_installation":{"dashboard_app_id":42,"surface":"conversation","compatibility_mode":"v2","enabled":true,"position":0,"capabilities":["account:read","current_user:read","current_user:email","permissions:read","appearance:read","installation:read","conversation:read","contact:read","contact:email","contact:phone","messages:read","identity:assertion"],"audience":{"type":"any","roles":["administrator"],"team_ids":[7],"user_ids":[]}}}'

# Update or reorder. Send the lock_version returned by the most recent response.
curl -sS -X PATCH "https://support.example.com/api/v1/accounts/1/dashboard_app_installations/9" \
  -H "api_access_token: API_TOKEN" -H "Content-Type: application/json" \
  --data '{"dashboard_app_installation":{"sidebar_category":"applications","sidebar_icon":"rocket","position":1,"lock_version":3}}'

# Delete.
curl -sS -X DELETE "https://support.example.com/api/v1/accounts/1/dashboard_app_installations/9" \
  -H "api_access_token: API_TOKEN"
```

The list returns `{ "payload": [...], "meta": { "revision": N } }`; create, show and update return
`{ "payload": { ... } }`; delete returns `204`. An installation includes its app id/title/URL,
`surface`, `compatibility_mode`, `enabled`, operational `visible`, `position`, sidebar-only
`sidebar_category` and `sidebar_icon` on sidebar resources, `capabilities`, optional administrator-only `audience`,
`transport_security`, `warnings`, `lock_version` and `updated_at`.

`POST /api/v1/accounts/:account_id/dashboard_app_installations/:id/identity_assertion` is the
authenticated call used by `getIdentityAssertion`; it enforces the feature, active V2/dual
installation, audience and capabilities. Apps should prefer the SDK. Server-to-server validation uses
the public `POST /dashboard-apps/v2/assertions/introspect` endpoint described above.

You can also create an app and initial installations atomically by adding an `installations` array to
`POST /dashboard_apps`. With the feature enabled, omitting the property creates the default
`conversation + legacy + all` installation; an empty array creates only the app. With the feature
disabled, sending `installations` is rejected and omitting it preserves legacy creation. One app can
have one installation per surface.

### 3. Use the account MCP tools

Enable **Dashboard Apps** in the MCP profile. The module stays in the existing
`channels_integrations` group and preserves the existing app-definition tools. It adds:

| Tool | Effect | Read-only profile |
|---|---|---|
| `list_dashboard_app_installations` | Lists the acting user's visible installations | Available |
| `create_dashboard_app_installation` | Creates an installation | Hidden |
| `update_dashboard_app_installation` | Updates category, icon, position, audience, or bridge | Hidden |
| `delete_dashboard_app_installation` | Deletes an installation | Hidden |

MCP uses the same account scope, policy, audience filtering, feature switch and validation errors as
REST. The direct REST show route and PUT alias are intentionally not separate MCP tools; list is the
canonical read and PATCH is the canonical update/reorder operation. Identity minting is not an MCP
tool: it belongs to an embedded human session, while MCP already has its own authenticated principal.

## Settings & options

- Surfaces: `conversation` or `sidebar`.
- Compatibility modes: `legacy`, `v2` or `dual`.
- Capabilities: required core; conversation/contact/messages only for `conversation`; optional personal
  data `current_user:email`, `contact:email`, `contact:phone`; optional signed identity
  `identity:assertion`. Omitting them applies the complete useful default for the selected surface;
  an explicit empty array is rejected with `invalid_capabilities`. Remove only optional grants and
  keep every capability required by the selected surface.
- Audience: `{ "type": "all" }`, or `{ "type": "any", "roles": [...], "team_ids": [...],
  "user_ids": [...] }`. Valid roles are `agent` and `administrator`; `any` requires at least one
  non-empty selector and grants access when any selector matches.
- Sidebar category: `support`, `contacts_crm`, `applications`, `commercial`, `productivity`,
  `automation`, `growth`, or `analytics_config`; accepted only for `sidebar`, with `productivity` as
  the default. The `applications` group sits directly below Contacts & CRM and is omitted when empty.
- Sidebar icon: `panels_top_left`, `layout_dashboard`, `app_window`, `boxes`, `briefcase`, `bot`,
  `calendar_days`, `chart_no_axes_combined`, `circle_dollar_sign`, `clipboard_list`, `database`,
  `folder`, `globe_2`, `headphones`, `life_buoy`, `messages_square`, `package`, `rocket`,
  `shopping_bag`, `sparkles`, `workflow`, or `wrench`; accepted only for `sidebar`, with
  `panels_top_left` as the default.
- Position: a zero-based value normalized within a surface and, for the sidebar, within its category.
- Concurrency: send `lock_version` on updates; `409 stale_installation` means reload and retry from the
  current response rather than overwriting another administrator's change.
- Stable validation errors include `invalid_audience`, `invalid_capabilities`, `invalid_sidebar_category`, `invalid_sidebar_icon`,
  `installation_already_exists`,
  `invalid_dashboard_app_url`, `dashboard_app_migration_required`, `unsafe_same_origin_v2` and
  `resource_not_found`. The migration error requires one HTTP(S) frame; query strings and fragments are valid.
- SDK limits include 64 KiB messages, 32 subscriptions, 30 commands per 10 seconds and frame height
  160–2,000 px.
- Handshake deadlines are **two** independent timers. The dashboard opens the window when the frame
  loads and re-sends the invitation for up to 10 seconds. The SDK's `connect()` has its own 10-second
  limit counted from the call, adjustable with `connect({ timeoutMs })`. Raising the SDK one does not
  help: the dashboard stops retrying first.
- Lifecycle: the SDK dispatches window events `conversalabs.dashboard-apps:bridge.connected`,
  `:bridge.paused`, `:bridge.resumed` and `:context.resynced`. Listen to them to pause work when the
  tab loses focus and to resynchronize. The `disconnected` code means every later command will be
  rejected; reconnect instead of retrying the command.

### Error codes and the first thing to check

| Code | Means | Start with |
|---|---|---|
| `handshake_timeout` | The dashboard retried for 10 s and the app never answered | Does the app call `connect()` on load? Does the loaded bundle expose `connect`? |
| `invalid_origin` | Configured or bridge origin is invalid | HTTP(S) URL without credentials; in V2 the origin must differ from the dashboard |
| `frame_blocked` | The browser refused the embed | `frame-ancestors` and `X-Frame-Options` on the app server |
| `insecure_transport_blocked` | HTTP app inside an HTTPS dashboard | Publish the app over HTTPS |
| `unsupported_version` | App and dashboard share no bridge version | Use the SDK from the same deployment |
| `unsupported_command` | Command outside the allowlist | Use only the announced commands |
| `invalid_payload` | Message outside the contract | Version, sequence, fields and size |
| `invalid_sequence` | Message out of order | Do not reimplement the protocol by hand; use the SDK |
| `rate_limited` | More than 30 commands in 10 s | Lower the frequency and group subscriptions |
| `disconnected` | The bridge closed | Reconnect; pending commands do not come back |
| `user_denied` | The person declined the confirmation | Expected on `openLink`; do not insist |
| `capability_denied` | Capability not granted | Enable it on the installation; contact data exists only on conversation |
| `identity_unavailable` | The assertion could not be issued | Feature, V2/dual mode, active installation and audience |

## Use cases

- Render live conversation context while keeping CRM mutations in an audited backend.
- Provision conversation and sidebar installations from an internal administration service.
- Let a read-only AI profile inventory visible apps without granting create, update or delete.
- Use MCP write tools in a restricted administrator profile for controlled configuration automation.

## Tips, limits & best practices

- Pin `dashboardOrigin` to the exact HTTPS origin. Never use `*`, accept untrusted `postMessage`
  origins or implement the wire protocol manually when the SDK is available.
- Static query strings and fragments are preserved. In V2/dual the host adds and overwrites only
  `cl_dashboard_origin`, `cl_installation_id`, `cl_account_id`, `cl_surface`, `cl_protocol` and
  `cl_locale`; treat every `cl_*` name as reserved. The host removes static `cl_*` values before writing
  those six parameters. Never put tokens, assertions, secrets or personal
  data in the URL or browser bundle.
- Keep REST and MCP credentials server-side, rotate them, and separate read-only inventory profiles
  from administrative profiles.
- Verify audience on the server. Hiding a tab in the iframe is not authorization.
- `openLink` always shows a native host confirmation. The link opens only after the person clicks
  **Open link**; an iframe-provided timestamp is never trusted as proof of a gesture.
- Handle `DashboardAppSDKError.code`, disconnects, rate limits and resynchronization explicitly.
- Treat `transport_security: insecure_http` and the `insecure_http` warning as a migration signal,
  not approval for production use.

## Troubleshooting

- **`handshake_timeout`**: the dashboard re-sent the invitation for 10 seconds and the app never
  answered. Start with the app, not the network: does it call `connect()` as soon as its page loads?
  Does the bundle it loaded expose `connect`? Only then check the origin and V2/dual mode. A frame
  that stays blank is a different problem — see `frame_blocked`.
- **Dual mode hides a V2 failure**: under Dual, agents keep working through the legacy lane and the
  surface reads ready even when the V2 bridge is dead. The **Test** dialog reports each bridge
  separately; to see the raw error, switch the installation to **V2** temporarily.
- **`invalid_origin`**: use an HTTP(S) URL without embedded credentials, and keep V2 on an origin
  different from the dashboard.
- **`unsupported_version` or `unsupported_command`**: use the SDK bundle from the same deployment and
  act only on announced capabilities.
- **`capability_denied`**: enable the requested installation capability; contact email/phone exists
  only on the conversation surface.
- **`identity_unavailable` or inactive introspection**: request a fresh assertion and verify feature,
  V2/dual mode, enabled installation, audience, membership and exact app origin.
- **`rate_limited` or `invalid_payload`**: reduce command frequency and message/subscription size.
- **REST/MCP returns 404**: check account id, installation id and the native-surfaces feature. Audience
  filtering can also make a resource invisible to the acting user.
- **REST/MCP returns 403**: the authenticated or acting user is neither an account administrator nor
  assigned a custom role with `integration_manage`.
- **409 on update**: fetch the current installation and retry with its `lock_version`.
- **422 on create/update**: inspect the stable `error` value; check URL, unique app/surface pair,
  sidebar category,
  audience selectors and same-origin V2 restrictions.

## See also

- [Dashboard Apps: native surfaces, audience and security](/hc/ajuda/articles/administration-dashboard-apps-native-surfaces-en)
- [REST API, tokens and webhooks](/hc/ajuda/articles/api-developers-rest-tokens-webhooks-en)
- [API reference (Swagger / OpenAPI)](/hc/ajuda/articles/api-developers-swagger-reference-en)
- [Native MCP: connections, server and clients](/hc/ajuda/articles/api-developers-mcp-server-and-client-en)