# La instalación y el canje del código

Qué recibe tu dirección de instalación, cómo canjear el código por las credenciales de cada empresa, cómo guardarlas y cómo se vuelve a conectar.

Cuando una empresa instala tu aplicación, UTILIA OS crea una instalación y manda a la persona a tu **dirección de instalación** con un código de un solo uso. Tu servidor canjea ese código y recibe, una sola vez, la clave de API y la clave de firma de esa instalación. Hasta que no lo canjea, la instalación no funciona: para la empresa está **Conectando** y, si pasan 30 minutos, **Falta conectar**.

## La conversación completa

**Instalar y canjear el código**

Participantes: Empresa que instala, UTILIA OS y Tu servidor.

1. Empresa que instala → UTILIA OS: Instalar y seguir en acme.example. Acepta los permisos y elige la persona responsable.
2. UTILIA OS (por su cuenta): Crea la instalación y un código válido 30 minutos.
3. Empresa que instala → Tu servidor: `GET dirección de instalación?code=…`. Con installation_id, return_url y, si lo había, state.
4. Tu servidor → UTILIA OS: `POST /external/v1/app-installations/exchange`. Con la clave de tu aplicación y el code.
5. UTILIA OS → Tu servidor (respuesta): installation y credentials. Solo esta vez.
6. UTILIA OS (por su cuenta): La instalación funciona y la pantalla de la empresa se actualiza sola.
7. UTILIA OS → Tu servidor: Aviso APP_INSTALLED. Firmado con la clave de firma de la instalación.
8. Tu servidor → Empresa que instala (respuesta): Redirige a return_url. O a tu configuración, si la persona tiene algo que configurar.

_Quien instala nunca ve una clave: las credenciales solo llegan a tu servidor._

## La dirección de instalación

Es la que declara tu ficha. Tiene que ser HTTPS y estar en tu dominio verificado. UTILIA OS la abre con estos parámetros y conserva los que ya llevara:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `code` | `string` | Sí | Código de un solo uso, del tipo uic\_…. Vale 30 minutos y solo lo puede canjear la aplicación que publica la ficha. |
| `installation_id` | `string` | Sí | Identificador de la instalación. Es el mismo que devuelve el canje, el de la cabecera X-Utilia-Installation-Id de los avisos y el de GET /external/v1/app-installations/:id. |
| `return_url` | `string` | Sí | Página de UTILIA OS a la que llevar a la persona al terminar. La fija UTILIA OS: con sesión abre el detalle de la instalación; sin sesión dice que ya puede cerrar la ventana. |
| `state` | `string` | No | Solo si la instalación empezó desde tu enlace «Instalar en UTILIA OS» con un state (hasta 500 caracteres). Te llega tal cual. |

Por ejemplo, con la dirección `https://acme.example/utilia/install`:

```text
https://acme.example/utilia/install?code=uic_X9f…&installation_id=cm3k9…&return_url=https%3A%2F%2Fos.utilia.ai%2Fapps%2Finstalled%3Finstallation_id%3Dcm3k9…
```

Nadie de la empresa tiene que copiar nada: la persona pulsa **Instalar y seguir en acme.example**, llega a tu web y, cuando tu servidor termina, vuelve a UTILIA OS. Si la conexión falla o la persona cierra la ventana, el código sigue vivo hasta que caduca.

### Empezar desde tu web

Si quieres poner en tu web un botón **Instalar en UTILIA OS**, enlaza la ficha de tu aplicación con `install=1`, que abre directamente la hoja de instalación, y, si quieres reconocer la vuelta, con tu propio `state`:

```text
https://os.utilia.ai/dashboard/settings/apps/explore/acme-recordatorios?install=1&state=sesion_8f2c
```

La dirección de la ficha la copias con **Compartir el enlace** o **Copiar el enlace** desde la propia ficha. Si la ficha es «Solo con el enlace», esa dirección lleva un sufijo aleatorio y es el único camino para llegar a ella. Comprueba al volver que el `state` es uno que tú generaste.

## Canjear el código

El canje se hace **solo desde tu servidor**, con la clave de API de la aplicación que publicó la ficha en `X-Api-Key`. Una petición que llega con las cabeceras de un navegador (`Origin` o `Sec-Fetch-Site`) se rechaza con `403 EXCHANGE_FROM_BROWSER`, aunque el código sea bueno. Lo abre el permiso de la API «Recibir las credenciales de cada instalación» (`apps:installations:exchange`), que tiene la aplicación que publica la ficha.

### curl

```bash
curl -X POST 'https://os.utilia.ai/api/external/v1/app-installations/exchange' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_la_clave_de_tu_aplicacion' \
  -d '{ "code": "uic_X9f…" }'
```

### Node.js

```javascript
const respuesta = await fetch('https://os.utilia.ai/api/external/v1/app-installations/exchange', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.UTILIA_APP_KEY,
  },
  body: JSON.stringify({ code }),
});
const cuerpo = await respuesta.json();
if (!respuesta.ok) throw new Error(`${cuerpo.errorCode}: ${cuerpo.message}`);
const { installation, credentials } = cuerpo;
```

### Python

```python
import os

import requests

respuesta = requests.post(
    "https://os.utilia.ai/api/external/v1/app-installations/exchange",
    headers={"X-Api-Key": os.environ["UTILIA_APP_KEY"]},
    json={"code": code},
    timeout=15,
)
cuerpo = respuesta.json()
if not respuesta.ok:
    raise RuntimeError(f"{cuerpo['errorCode']}: {cuerpo['message']}")
installation, credentials = cuerpo["installation"], cuerpo["credentials"]
```

### SDK

```typescript
import { UtiliaSDK } from '@utilia-os/sdk-js';

const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_APP_KEY!, // la clave de tu aplicación
});

const { installation, credentials } = await sdk.appInstallations.exchange(code);
```

En Python es `await sdk.app_installations.exchange(code)`. Los dos llegan con la versión 4.7.0 de los SDK, sin publicar todavía: mientras tanto, usa la petición directa.

La respuesta, con `200` y `Cache-Control: no-store`:

```json
{
  "installation": {
    "id": "cm3k9x2a40001qz8h7d1v2b3c",
    "organization": { "id": "cm1a2b3c40000ab12cd34ef56", "name": "Ferretería López" },
    "scopes": ["crm:clients:read", "crm:contacts:read"],
    "webhookEvents": ["CLIENT_CREATED", "CONTACT_CREATED", "APP_INSTALLED", "APP_UNINSTALLED", "APP_ACCESS_CHANGED", "APP_SUSPENDED", "APP_REINSTATED"],
    "installedAt": "2026-11-03T09:14:27.000Z"
  },
  "credentials": {
    "apiKey": "utl_k3J9…",
    "webhookSigningSecret": "9f86d081884c7d659a2feaa0c55ad015…"
  }
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `installation.id` | `string` | Sí | La instalación. Guárdalo: es la clave con la que encuentras sus credenciales. |
| `installation.organization` | `object` | Sí | id y name de la empresa que instaló. El id es el organizationId que traen sus avisos. |
| `installation.scopes` | `string[]` | Sí | Permisos de la API que concedió la empresa: los que pide tu ficha. |
| `installation.webhookEvents` | `string[]` | Sí | Avisos que recibirá tu servidor por esta instalación: los de tu ficha que cubren esos permisos y los cinco del ciclo de vida. Vacío si la ficha no tiene dirección de avisos. |
| `installation.installedAt` | `string` | Sí | Cuándo se instaló, en ISO 8601. |
| `credentials.apiKey` | `string` | Sí | Clave de API de la instalación (utl\_…), para la cabecera X-Api-Key. No se vuelve a enseñar. |
| `credentials.webhookSigningSecret` | `string` | Sí | Clave de firma de los avisos de esta instalación (HMAC-SHA256). No se vuelve a enseñar. |

Justo después, si es la primera vez que se conecta, llega a tu dirección de avisos el aviso `APP_INSTALLED`, firmado con esa clave de firma. Lo explica [Avisos de cada instalación](https://os.utilia.ai/docs/developers/apps-platform/webhooks).

## Guardar las credenciales de cada instalación

Cada empresa es una instalación distinta, con su clave y su clave de firma. Guárdalas por instalación, nunca juntas en una sola variable de entorno:

- **La clave de API**, cifrada, para llamar en nombre de esa empresa.
- **La clave de firma**, cifrada, para comprobar sus avisos. La encuentras por la cabecera `X-Utilia-Installation-Id`.
- **El identificador de la instalación y el de la empresa**, sin cifrar, para buscar.
- **Los permisos y los avisos**, para saber qué puedes pedir sin probar.

Si el canje responde bien pero tu base de datos falla al guardar, no hay segunda oportunidad: el código ya está gastado. Guarda primero y, si falla, pide a la empresa que vuelva a conectar. Las reglas de custodia están en [Seguridad: credenciales y avisos](https://os.utilia.ai/docs/developers/apps-platform/security).

Con la clave, cada llamada actúa con los permisos de la persona responsable que eligió la empresa, recortados por los permisos concedidos, por su plan y por los datos que esa persona puede ver. Si la empresa se queda sin persona responsable, sus llamadas responden `403 APP_RESPONSIBLE_INACTIVE`; si deja de tener un aplicativo que necesitas, las de ese aplicativo responden `403 MODULE_NOT_ENABLED`. En los dos casos se arregla en la empresa, no en tu código.

## Volver a conectar

La empresa genera un código nuevo desde el detalle de la instalación, en **Instaladas**:

- **Conectar con Acme**, cuando la instalación está en **Falta conectar** porque el primer código caducó.
- **Volver a conectar con Acme**, con la instalación funcionando, si tú se lo pides: por ejemplo, porque perdiste las credenciales o crees que se han expuesto.

En los dos casos la persona vuelve a tu dirección de instalación con un `code` nuevo, y el canje te entrega una **clave de API nueva** y la **misma clave de firma**. Si la instalación ya funcionaba, la clave anterior sigue valiendo siete días para que cambies la de tu servidor sin cortes; si nunca se canjeó, se sustituye en el acto. El aviso `APP_INSTALLED` solo llega la primera vez que se conecta. Haz que tu servidor sustituya la clave de esa instalación, buscándola por su identificador, en lugar de crear otra.

## Estados de una instalación

Así ves cada instalación con `GET /external/v1/app-installations`:

**Estados de una instalación**

1. **Conectando** (`CONNECTING`). Hay un código vigente sin canjear.
   - **Falta conectar** (`NEEDS_CONNECTION`). Se llega desde Conectando cuando: Pasan 30 minutos sin canje. La empresa tiene que pulsar Conectar.
2. **Activa** (`ACTIVE`). Se llega desde Conectando cuando: Tu servidor canjea el código. Su clave funciona y le llegan avisos.
   - **Pide más permisos** (`ACCESS_PENDING`). Se llega desde Activa cuando: UTILIA aprueba una versión con más permisos. Sigue con los que tenía.
   - **Suspendida por UTILIA** (`SUSPENDED`). Se llega desde Activa cuando: UTILIA suspende la aplicación. No puede acceder a los datos.
3. **Desinstalada** (`UNINSTALLED`, estado final). Se llega desde Activa cuando: La empresa la desinstala. Su clave deja de valer en el acto.

Otras transiciones:

- Falta conectar → Conectando: La empresa pulsa Conectar y recibes un código nuevo.
- Pide más permisos → Activa: La empresa acepta los permisos nuevos.
- Suspendida por UTILIA → Activa: UTILIA la reanuda; si nunca se conectó, vuelve a Falta conectar.

_La empresa puede desinstalarla en cualquier estado._

`installedVia` dice de dónde vino: `CATALOG` (desde Explorar), `LINK` (una ficha «Solo con el enlace») o `TEST` (una instalación de prueba).

## Si el canje falla

Ningún error del canje se arregla repitiendo la misma petición. Cada código tiene su apartado, con el ancla que trae `docsUrl`:

| errorCode | HTTP | Qué pasa | Qué hacer |
| - | - | - | - |
| [`EXCHANGE_FROM_BROWSER`](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#exchange_from_browser) | 403 | La petición llegó con `Origin` o `Sec-Fetch-Site`. | Haz el canje desde tu servidor, nunca desde la página. |
| [`INSTALLATION_CODE_WRONG_APP`](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#installation_code_wrong_app) | 403 | El código es de una ficha de otra aplicación tuya. | Usa la clave de la aplicación que publicó esa ficha. |
| [`INSTALLATION_CODE_INVALID`](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#installation_code_invalid) | 404 | El código no existe para tu aplicación, o la instalación ya está desinstalada o suspendida. | Comprueba que lo copiaste entero. Si sigue, pide a la empresa que vuelva a conectar. |
| [`INSTALLATION_CODE_USED`](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#installation_code_used) | 409 | El código ya se canjeó una vez. | Si perdiste las credenciales, pide a la empresa que vuelva a conectar. |
| [`INSTALLATION_CODE_EXPIRED`](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#installation_code_expired) | 410 | Pasaron los 30 minutos. | La empresa pulsa **Conectar con** tu aplicación en Instaladas. |
| [`INSUFFICIENT_SCOPE`](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#insufficient_scope) | 403 | A tu aplicación le falta `apps:installations:exchange`. | Comprueba que llamas con la clave de la aplicación que publica la ficha. |

Un `INSTALLATION_CODE_USED` justo después de un canje correcto suele ser la misma dirección abierta dos veces (la persona recargó la página o un navegador la abrió por adelantado). Si ya tienes las credenciales de esa `installation_id`, ignóralo y lleva a la persona a `return_url`.

La referencia de las tres rutas, con la consulta de tus instalaciones, está en [Referencia de las instalaciones](https://os.utilia.ai/docs/developers/apps-platform/installations-api).
