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_managecreate, 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_surfacesenabled 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 anapi_access_tokenfor REST management. - For MCP, an account MCP profile whose acting user has that permission and whose selected modules include Dashboard Apps.
- CSP
frame-ancestorsconfigured to allow the exact Conversa Labs origin without a conflictingX-Frame-Optionsresponse 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.idandaccount.nameto identify the account;current_user.id,current_user.name,current_user.roleandcurrent_user.avatar_urlto identify the person who opened the app;installation.id,surface, and sidebar-onlysidebar_categoryandsidebar_iconto identify the installation, location, native category, and selected icon;- on the conversation surface,
conversation,contact,permissionsand 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:
conversationorsidebar. - Compatibility modes:
legacy,v2ordual. - Capabilities: required core; conversation/contact/messages only for
conversation; optional personal datacurrent_user:email,contact:email,contact:phone; optional signed identityidentity:assertion. Omitting them applies the complete useful default for the selected surface; an explicit empty array is rejected withinvalid_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 areagentandadministrator;anyrequires at least one non-empty selector and grants access when any selector matches. - Sidebar category:
support,contacts_crm,applications,commercial,productivity,automation,growth, oranalytics_config; accepted only forsidebar, withproductivityas the default. Theapplicationsgroup 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, orwrench; accepted only forsidebar, withpanels_top_leftas the default. - Position: a zero-based value normalized within a surface and, for the sidebar, within its category.
- Concurrency: send
lock_versionon updates;409 stale_installationmeans 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_v2andresource_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 withconnect({ 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.resumedand:context.resynced. Listen to them to pause work when the tab loses focus and to resynchronize. Thedisconnectedcode 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
dashboardOriginto the exact HTTPS origin. Never use*, accept untrustedpostMessageorigins 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_protocolandcl_locale; treat everycl_*name as reserved. The host removes staticcl_*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.
openLinkalways 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_httpand theinsecure_httpwarning 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 callconnect()as soon as its page loads? Does the bundle it loaded exposeconnect? Only then check the origin and V2/dual mode. A frame that stays blank is a different problem — seeframe_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_versionorunsupported_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_unavailableor inactive introspection: request a fresh assertion and verify feature, V2/dual mode, enabled installation, audience, membership and exact app origin.rate_limitedorinvalid_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
errorvalue; check URL, unique app/surface pair, sidebar category, audience selectors and same-origin V2 restrictions.