Dashboard Apps SDK, REST API and MCP

Conversa Labs

Conversa Labs

Last updated on Aug 26, 2026

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.

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:

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.

# 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