## Descripción general

Los Dashboard Apps nativos tienen tres contratos separados:

- El **SDK V2** del navegador entrega contexto limitado por capacidades, eventos de solo lectura,
  comandos seguros e identidad firmada opcional para el backend propio de la app.
- La **API REST** permite a administradores o roles personalizados con `integration_manage` crear,
  consultar, actualizar, reordenar y eliminar apps e
  instalaciones.
- El **servidor MCP** expone el mismo contrato con cuatro herramientas opcionales en
  **Canales e integraciones → Dashboard Apps**.

El SDK autentica solo una identidad breve de la app incrustada; no ofrece un proxy de API genérico.
Las escrituras deben pasar por tu backend, autenticado en REST con un token de privilegio mínimo.

## Requisitos previos

- `dashboard_apps_native_surfaces` habilitada en la cuenta.
- Dashboard App HTTPS en un origen diferente del dashboard de Conversa Labs para V2.
- Un administrador o rol personalizado con `integration_manage` y un `api_access_token` para gestión REST.
- Para MCP, perfil cuyo usuario actuante tenga ese permiso y que incluya **Dashboard Apps**.
- CSP `frame-ancestors` que permita el origen exacto de Conversa Labs, sin `X-Frame-Options` incompatible.

## Paso a paso

### 1. Conecta el SDK V2 en el navegador

Importa el SDK desde la ruta estable y versionada de tu despliegue de Conversa Labs. Redirige al build
actual con fingerprint y expone exportaciones con nombre del módulo JavaScript.

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

try {
  // El host inyecta parámetros cl_*; origen e instalación se descubren automáticamente.
  // Las opciones explícitas siguen disponibles para pruebas controladas.
  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('#documentacion').addEventListener('click', () => {
    client.openLink('https://app.ejemplo.com/docs');
  });

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

Usa las importaciones con nombre del módulo mostradas arriba. Son el contrato estable para aplicaciones
nuevas. Si tu aplicación no puede consumir exportaciones con nombre, el mismo archivo también expone
`window.ConversaLabsDashboardAppSDK`, con las mismas funciones.

**Llama a `connect()` apenas cargue la página**, no detrás de autenticación ni de una ida a tu backend.
El host inicia el handshake cuando el marco termina de cargar y reintenta hasta 10 segundos; una
aplicación que empieza a escuchar después de esa ventana recibe `handshake_timeout`. Un `import`
estático al inicio de un `<script type="module">` es la forma soportada. Un `import()` dinámico
funciona si se espera de inmediato — no dejes la carga del SDK para después de renderizar la pantalla.

`connect` lee `cl_dashboard_origin` y `cl_installation_id` de la URL, valida el origen exacto, negocia
protocolo `2.0` y resuelve tras el handshake. El cliente
expone `installationId`, `capabilities`, `connected`, `subscribe`, `unsubscribe`, `setHeight`,
`openLink`, `getIdentityAssertion` y `disconnect`.

Los eventos incluyen contexto, conversación, estado/asignación/etiquetas, mensajes, contacto/usuario,
idioma/tema/permisos e instalación. La conversación es omnicanal y funciona para correo, WhatsApp,
SMS y otros inboxes; la barra lateral omite conversación y contacto. Programa
según `capabilities`, campos opcionales y `context_revision` creciente; resincroniza datos antiguos.

#### Identidad segura para n8n y otros backends

No envíes identidad ni credenciales en la query string del iframe. En `context.initialized`, usa:

- `account.id` y `account.name` para identificar la cuenta;
- `current_user.id`, `current_user.name`, `current_user.role` y `current_user.avatar_url` para identificar
  a la persona que abrió la app;
- `installation.id`, `surface`, `sidebar_category` y `sidebar_icon` en barra lateral para identificar
  instalación, lugar, categoría nativa e icono elegido;
- en la superficie de conversación, `conversation`, `contact`, `permissions` y eventos de mensajes para
  el contexto operativo.

El bridge nunca expone tokens de usuario/sesión/CSRF, REST, MCP ni credenciales del proveedor del canal.
Cuando se conceden por instalación, `current_user:email` añade el correo del usuario actual;
`contact:email` y `contact:phone` añaden datos del contacto en conversación. Sin la capacidad, el campo
se omite del contexto y los eventos.

Para que un backend propio o webhook n8n verifique identidad sin confiar en IDs del navegador, pide
una aserción breve:

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

El backend envía `assertion` por POST a la `introspection_url` recibida, opcionalmente con
`expected_origin: "https://app.ejemplo.com"`. Una respuesta activa contiene claims de cuenta, usuario,
instalación, superficie y capacidades. La aserción expira en dos minutos y cada introspección revalida
feature, instalación, usuario activo, membresía, audiencia y origen. El verificador público rechaza
aserciones mayores de 16 KiB antes de verificar la firma. **No** es un token REST/MCP y no debe almacenarse
ni usarse como bearer. Datos y escrituras adicionales usan la credencial REST de privilegio mínimo o
el perfil MCP restringido del backend.

### 2. Gestiona instalaciones por REST

Todas las rutas pertenecen a la cuenta. Con la feature desactivada responde `404 resource_not_found`;
un miembro autenticado sin permiso de gestión recibe `403 forbidden` en esas operaciones.

```bash
# Lista instalaciones visibles; administradores también reciben audience.
curl -sS "https://soporte.ejemplo.com/api/v1/accounts/1/dashboard_app_installations?surface=conversation" \
  -H "api_access_token: API_TOKEN"

# Crea una instalación. dashboard_app_id queda inmutable.
curl -sS -X POST "https://soporte.ejemplo.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":[]}}}'

# Actualiza o reordena con el lock_version de la respuesta más reciente.
curl -sS -X PATCH "https://soporte.ejemplo.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}}'

# Elimina.
curl -sS -X DELETE "https://soporte.ejemplo.com/api/v1/accounts/1/dashboard_app_installations/9" \
  -H "api_access_token: API_TOKEN"
```

La lista devuelve `{ "payload": [...], "meta": { "revision": N } }`; crear, ver y actualizar
devuelven `{ "payload": { ... } }`; eliminar devuelve `204`. La instalación incluye app id/título/URL,
`surface`, `compatibility_mode`, `enabled`, `visible` operativo, `position`, `sidebar_category` y
`sidebar_icon` solo en barra lateral, `capabilities`, `audience` solo para administradores,
`transport_security`, `warnings`, `lock_version` y `updated_at`.

`POST /api/v1/accounts/:account_id/dashboard_app_installations/:id/identity_assertion` es la llamada
autenticada usada por `getIdentityAssertion`; aplica feature, instalación V2/dual activa, audiencia y
capacidades. Las apps deben preferir el SDK. La validación server-to-server usa el endpoint público
`POST /dashboard-apps/v2/assertions/introspect` descrito arriba.

También puedes crear app e instalaciones iniciales atómicamente con `installations` en
`POST /dashboard_apps`. Con la feature habilitada, omitir la propiedad crea la instalación por defecto
`conversation + legacy + all`; un array vacío crea solo la app. Con la feature deshabilitada, enviar
`installations` se rechaza y omitirla conserva la creación heredada. Cada app puede tener una
instalación por superficie.

### 3. Usa las herramientas MCP de la cuenta

Habilita **Dashboard Apps** en el perfil MCP. El módulo permanece en `channels_integrations`, conserva
las herramientas existentes de definición de app y añade:

| Herramienta | Efecto | Perfil de solo lectura |
|---|---|---|
| `list_dashboard_app_installations` | Lista instalaciones visibles al usuario actuante | Disponible |
| `create_dashboard_app_installation` | Crea una instalación | Oculta |
| `update_dashboard_app_installation` | Actualiza categoría, icono, posición, audiencia o bridge | Oculta |
| `delete_dashboard_app_installation` | Elimina una instalación | Oculta |

MCP usa el mismo ámbito, política, audiencia, feature y errores que REST. El detalle REST y el alias PUT
no son herramientas separadas: list es la lectura canónica y PATCH la actualización/reordenación
canónica. La emisión de identidad no es una herramienta MCP: pertenece a la sesión humana incrustada,
mientras MCP ya posee su propio principal autenticado.

## Configuración y opciones

- Superficies: `conversation` o `sidebar`.
- Modos: `legacy`, `v2` o `dual`.
- Capacidades: núcleo obligatorio; conversación/contacto/mensajes solo para `conversation`; datos
  personales opcionales `current_user:email`, `contact:email`, `contact:phone`; identidad opcional
  `identity:assertion`. Omitirlas aplica el valor completo y útil de la superficie; un array vacío
  explícito se rechaza con `invalid_capabilities`. Elimina solo concesiones opcionales y conserva
  todas las capacidades obligatorias para la superficie seleccionada.
- Audiencia: `{ "type": "all" }` o `{ "type": "any", "roles": [...], "team_ids": [...],
  "user_ids": [...] }`. Roles válidos: `agent` y `administrator`; `any` exige un selector no vacío y
  concede acceso si cualquiera coincide.
- Categoría lateral: `support`, `contacts_crm`, `applications`, `commercial`, `productivity`,
  `automation`, `growth` o `analytics_config`; solo se acepta en `sidebar` y el valor predeterminado
  es `productivity`. El grupo `applications` queda justo debajo de Contactos y CRM y se omite vacío.
- Icono lateral: `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` o `wrench`; solo se acepta en `sidebar`, con
  `panels_top_left` como valor predeterminado.
- Posición: índice desde cero normalizado por superficie y, en barra lateral, por categoría.
- Concurrencia: envía `lock_version`; `409 stale_installation` exige recargar y reintentar.
- Errores estables: `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` y `resource_not_found`. El error de
  migración exige un marco HTTP(S); query string y fragmento son válidos.
- Límites SDK: 64 KiB por mensaje, 32 suscripciones, 30 comandos por 10 segundos y altura
  160–2.000 px.
- Los plazos del handshake son **dos**, independientes. El panel abre la ventana cuando el marco carga
  y reenvía la invitación hasta 10 segundos. El `connect()` del SDK tiene su propio límite de 10
  segundos desde la llamada, ajustable con `connect({ timeoutMs })`. Subir el del SDK no ayuda: el
  panel deja de reintentar antes.
- Ciclo de vida: el SDK emite eventos de ventana `conversalabs.dashboard-apps:bridge.connected`,
  `:bridge.paused`, `:bridge.resumed` y `:context.resynced`. Escúchalos para pausar trabajo cuando la
  pestaña pierde el foco y para resincronizar. El código `disconnected` significa que todo comando
  posterior será rechazado; reconecta en lugar de reintentar el comando.

### Códigos de error y lo primero que revisar

| Código | Significa | Empieza por |
|---|---|---|
| `handshake_timeout` | El panel reintentó 10 s y la aplicación no respondió | ¿Llama a `connect()` al cargar? ¿El bundle cargado expone `connect`? |
| `invalid_origin` | Origen configurado o del bridge inválido | URL HTTP(S) sin credenciales; en V2 el origen debe diferir del panel |
| `frame_blocked` | El navegador rechazó la incrustación | `frame-ancestors` y `X-Frame-Options` en el servidor de la aplicación |
| `insecure_transport_blocked` | Aplicación HTTP dentro de un panel HTTPS | Publica la aplicación por HTTPS |
| `unsupported_version` | Aplicación y panel no comparten versión del bridge | Usa el SDK del mismo despliegue |
| `unsupported_command` | Comando fuera de la allowlist | Usa solo los comandos anunciados |
| `invalid_payload` | Mensaje fuera del contrato | Versión, secuencia, campos y tamaño |
| `invalid_sequence` | Mensaje fuera de orden | No reimplementes el protocolo a mano; usa el SDK |
| `rate_limited` | Más de 30 comandos en 10 s | Baja la frecuencia y agrupa suscripciones |
| `disconnected` | El bridge se cerró | Reconecta; los comandos pendientes no vuelven |
| `user_denied` | La persona rechazó la confirmación | Esperado en `openLink`; no insistas |
| `capability_denied` | Capacidad no concedida | Actívala en la instalación; los datos de contacto solo existen en conversación |
| `identity_unavailable` | No se pudo emitir la aserción | Feature, modo V2/dual, instalación activa y audiencia |

## Casos de uso

- Mostrar contexto de conversación manteniendo cambios del CRM en un backend auditable.
- Provisionar instalaciones desde un servicio administrativo interno.
- Permitir que un perfil IA de solo lectura inventaríe apps sin escritura.
- Usar MCP de escritura en un perfil administrador restringido para automatización controlada.

## Consejos, límites y buenas prácticas

- Fija `dashboardOrigin` al origen HTTPS exacto. No uses `*`, no aceptes orígenes `postMessage` no
  confiables ni reimplementes el protocolo si existe el SDK.
- Query string y fragmento estáticos se conservan. En V2/dual el host añade y sobrescribe solo
  `cl_dashboard_origin`, `cl_installation_id`, `cl_account_id`, `cl_surface`, `cl_protocol` y
  `cl_locale`; trata todo nombre `cl_*` como reservado. El host elimina los valores `cl_*` estáticos
  antes de escribir esos seis parámetros. Nunca pongas tokens, aserciones, secretos ni
  datos personales en la URL o bundle.
- Mantén credenciales REST/MCP en el servidor, rótalas y separa perfiles de inventario y administración.
- Autoriza audiencia en el servidor. Ocultar una pestaña no es autorización.
- `openLink` siempre muestra una confirmación nativa del host. El enlace solo se abre después de que
  la persona pulse **Abrir enlace**; nunca se confía en un timestamp del iframe como prueba de gesto.
- Maneja códigos de `DashboardAppSDKError`, desconexión, límites y resincronización.
- `insecure_http` es una señal de migración, no aprobación para producción.

## Solución de problemas

- **`handshake_timeout`**: el panel reenvió la invitación durante 10 segundos y la aplicación no
  respondió. Empieza por la aplicación, no por la red: ¿llama a `connect()` apenas carga la página?
  ¿El bundle que cargó expone `connect`? Solo después revisa el origen y el modo V2/dual. Un marco en
  blanco es otro problema — consulta `frame_blocked`.
- **El modo Dual oculta el fallo de V2**: con Dual, la atención sigue por el lane heredado y la
  superficie aparece lista aunque el bridge V2 esté muerto. El diálogo **Probar** muestra el resultado
  de cada bridge por separado; para ver el error crudo, cambia la instalación a **V2** temporalmente.
- **`invalid_origin`**: usa una URL HTTP(S) sin credenciales incrustadas y mantén V2 en un origen
  distinto al del panel.
- **`unsupported_version` o `unsupported_command`**: usa el SDK del mismo despliegue y capacidades anunciadas.
- **`capability_denied`**: habilita la capacidad solicitada; correo/teléfono del contacto existe solo
  en la superficie de conversación.
- **`identity_unavailable` o introspección inactiva**: pide una aserción nueva y verifica feature,
  modo V2/dual, instalación activa, audiencia, membresía y origen exacto.
- **`rate_limited` o `invalid_payload`**: reduce frecuencia, tamaño y suscripciones.
- **REST/MCP devuelve 404**: revisa cuenta, instalación y feature; la audiencia también puede ocultarla.
- **REST/MCP devuelve 403**: el usuario autenticado o actuante no es administrador ni posee un rol
  personalizado con `integration_manage`.
- **409 al actualizar**: obtiene el estado actual y repite con su `lock_version`.
- **422 al crear/actualizar**: revisa `error`, URL, par app/superficie, categoría lateral, audiencia y origen V2.

## Ver también

- [Dashboard Apps: superficies nativas, audiencia y seguridad](/hc/ajuda/articles/administration-dashboard-apps-native-surfaces-es)
- [API REST, tokens y webhooks](/hc/ajuda/articles/api-developers-rest-tokens-webhooks-es)
- [Referencia de API (Swagger / OpenAPI)](/hc/ajuda/articles/api-developers-swagger-reference-es)
- [MCP nativo: conexiones, servidor y clientes](/hc/ajuda/articles/api-developers-mcp-server-and-client-es)