# Darle acciones a un agente de Lelemon desde otra aplicación

Documento para el equipo que va a escribir la integración. Describe cómo registrar una acción HTTP —una llamada a un sistema de ustedes que el agente puede hacer en medio de una conversación— y cómo concedérsela o quitársela a un agente. Alcanza para implementar el lado de ustedes sin preguntarnos nada.

Base de la API de Lelemon: `https://api.lelemon.cl`

## Qué resuelve esto

Un agente contesta con lo que sabe. Con una acción, además **hace**: abre un ticket en el sistema de ustedes, consulta el estado de un pedido, agenda algo en su calendario. La acción es una URL de ustedes, un método, y un esquema de los parámetros que el agente va a completar con lo que entendió de la conversación.

Son dos recursos distintos, y conviene tenerlos separados desde el principio:

- **La acción** es de la organización. Se registra una vez, con su URL y su credencial.
- **La concesión** es por agente. Una acción registrada no la usa nadie hasta que se le concede a un agente concreto, de a una.

Todo va con la credencial de organización en la cabecera `Authorization`:

```
Authorization: Bearer lel_a1b2c3d4e5f6_<...>
```

Alcances: `actions:read` para ver, `actions:write` para registrar, dar de baja, conceder y quitar. `actions:write` es el permiso más delicado de la API —una acción HTTP manda datos de contactos de ustedes al sistema que la acción apunte— así que se lo dan sólo a integraciones que ya administran.

Una credencial con `actions:write` sólo registra acciones hacia los sitios que tiene permitidos. La lista la escribe una persona con rol de administrador en Integraciones, API, al emitir la credencial o después, y la credencial no la puede cambiar. Un sitio es esquema, host y puerto: `https://n8n.ejemplo.cl` no permite `http://n8n.ejemplo.cl` ni `https://otro.n8n.ejemplo.cl`. Así, si la credencial se filtra, quien la tenga no puede crear una acción que mande los datos a un servidor suyo.

---

## 1. Si van a dejar registrada la autorización: el texto

```
GET https://api.lelemon.cl/v1/external/actions/authorization
```

No pide alcance: alcanza con una credencial viva.

Una acción manda datos de las conversaciones —nombre, RUT, teléfono, lo que la persona cuente— al sistema que ustedes indiquen. La Ley 21.719 (art. 15 bis) pide que el responsable de esos datos nos haya autorizado a entregarlos: quién, para qué, hacia dónde. Esa autorización puede constar en el contrato marco que tienen con nosotros, y por eso **registrar la acción no la exige**. Si prefieren dejarla registrada acción por acción —con versión del texto, país de destino y finalidad, consultable después—, la mandan en el mismo cuerpo, y para eso está este endpoint: lo que aceptan tiene versión, y la versión viaja en el cuerpo. Si no van a mandarla, salten al paso 2.

### La respuesta

```json
{
  "version": "2026-08-1",
  "vigente": {
    "version": "2026-08-1",
    "texto": "Autorizo a Lelemon a enviar a este destino los datos que el agente saque de las conversaciones: ...",
    "detalle": "Cuando el agente decide usar esta acción, arma los parámetros con lo que entendió ..."
  },
  "fueraDeChile": {
    "version": "2026-08-1",
    "texto": "Este sistema está fuera de Chile, así que los datos salen del país. ...",
    "detalle": "La ley permite sacar datos de Chile cuando ..."
  },
  "paises": ["chile", "fuera_de_chile", "desconocido"]
}
```

`version` es lo que va en `autorizacionVersion` si registran la autorización. Guárdenla en la configuración de su integración, no en el código: cuando el texto cambie, el registro empieza a contestar `400` nombrando la versión que mandaron y la vigente, y la corrección es volver a leer este endpoint y actualizar ese valor.

`fueraDeChile` es un segundo texto, aparte. Si el sistema al que apunta la acción está alojado fuera de Chile —o no saben dónde—, los datos salen del país y eso es una transferencia internacional (art. 27): hay que confirmarla por separado, y queda registrada por separado.

---

## 2. Registrar una acción

```
POST https://api.lelemon.cl/v1/external/actions
```

Alcance: `actions:write`.

### El cuerpo

| Campo | Tipo | Límite | Obligatorio | Qué hace |
|---|---|---|---|---|
| `slug` | texto | 64 | sí | Identificador de la acción. Es con lo que se le concede a un agente, y es la llave para reintentar sin duplicar: mandar el mismo `slug` dos veces actualiza la misma acción con lo que manden, y conserva lo que no manden (las cabeceras, si no vienen, siguen). |
| `name` | texto | 100 | sí | Cómo se llama. |
| `description` | texto | 2000 | sí | Lo que lee el agente para decidir cuándo usarla. Sin esto no la usa nunca. Escríbanla para el agente, no para una persona: "Consulta el estado de un pedido por su número. Usar cuando el cliente pregunte dónde está su compra." |
| `url` | texto | 2000 | sí | A dónde llama. `http` o `https`, pública y alcanzable: una dirección privada o que no resuelve da `400`. **No vuelve en ninguna respuesta**: de la dirección sólo devolvemos el origen. |
| `method` | texto | — | sí | `GET`, `POST`, `PUT`, `PATCH` o `DELETE`. |
| `headers` | objeto | — | no | Nombre de cabecera a valor. Acá viaja la credencial del sistema de ustedes. Se cifra antes de guardarse y no vuelve nunca, ni siquiera enmascarada. |
| `parameters` | objeto | — | no | JSON Schema de los parámetros que el agente completa. Los **nombres** de estos parámetros son lo que queda registrado como "qué datos salen". |
| `timeout` | entero | ms | no | Cuánto esperamos la respuesta de ustedes. |
| `paraClientes` | booleano | — | no | A qué agentes se les puede conceder. Sin mandarlo, a cualquiera: a quién se la dan se decide al concederla (paso 4). `true`: sólo a los que atienden desconocidos por WhatsApp. `false`: sólo al asistente interno de ustedes. Tiene que ser un booleano de verdad: `"false"` entre comillas da `400`, no lo convertimos. |
| `purpose` | texto | 500 | no* | Para qué es. Parte de la autorización: la ley pide que el encargo declare su finalidad. |
| `destinationCountry` | texto | — | no* | Dónde está alojado el sistema: `chile`, `fuera_de_chile` o `desconocido`. Lo declaran ustedes; no lo detectamos, porque con un CDN adelante el DNS miente. `desconocido` cuenta como fuera. |
| `autorizacionVersion` | texto | — | no* | La `version` que devolvió el paso 1. |
| `confirmaSalidaDelPais` | booleano | — | con la autorización, si el destino no es `chile` | La segunda confirmación. Sin ella, una autorización con destino fuera de Chile da `400`. |

\* La autorización es **un bloque**: o no mandan nada de `purpose`, `destinationCountry` y `autorizacionVersion` —y la acción se registra igual, sin autorización registrada— o mandan los tres. Dos de tres es `400` que dice cuál falta: una autorización a medias, con la finalidad en blanco o el país sin decir, sería peor que ninguna.

### La respuesta

`201` con la acción, sin la URL:

```json
{
  "slug": "crear_ticket",
  "name": "Crear un ticket",
  "description": "Abre un ticket en el sistema de soporte con lo que cuenta el cliente.",
  "method": "POST",
  "origin": "https://api.suempresa.cl",
  "headerNames": ["Authorization"],
  "parameters": { "type": "object", "properties": { "resumen": { "type": "string" } } },
  "audience": null,
  "status": "active"
}
```

`audience` es a quién se le puede conceder: `null` es cualquiera (lo que queda sin `paraClientes`), `external` sólo los que atienden clientes, `internal` sólo el asistente interno.

`origin` y no `url`, y `headerNames` y no `headers`, a propósito: en un webhook de n8n el *path* es el token, y `actions:read` es el alcance que se le da a un auditor.

### Ejemplo con curl

Con la autorización registrada. Sin ella, saquen las tres últimas líneas del cuerpo y el `VERSION`.

```bash
VERSION=$(curl -s https://api.lelemon.cl/v1/external/actions/authorization \
  -H "Authorization: Bearer $LELEMON_API_KEY" | jq -r .version)

curl -X POST https://api.lelemon.cl/v1/external/actions \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"slug\": \"crear_ticket\",
    \"name\": \"Crear un ticket\",
    \"description\": \"Abre un ticket en soporte con lo que cuenta el cliente. Usar cuando reporte un problema.\",
    \"url\": \"https://api.suempresa.cl/tickets\",
    \"method\": \"POST\",
    \"headers\": { \"Authorization\": \"Bearer $TOKEN_DE_USTEDES\" },
    \"parameters\": { \"type\": \"object\", \"properties\": { \"resumen\": { \"type\": \"string\" } }, \"required\": [\"resumen\"] },
    \"purpose\": \"Abrir tickets de soporte con lo que reporta el cliente.\",
    \"destinationCountry\": \"chile\",
    \"autorizacionVersion\": \"$VERSION\"
  }"
```

### Códigos de estado

| Código | Cuándo |
|---|---|
| `201` | Registrada (o actualizada, si el `slug` ya existía). |
| `400` | Falta un campo de la acción, la autorización vino a medias, la `autorizacionVersion` no es la vigente, el destino sale de Chile sin `confirmaSalidaDelPais`, `paraClientes` no es un booleano, o la URL no se puede usar (privada, no resuelve, no es `http`/`https`). El mensaje dice cuál. |
| `401` | Credencial ausente, revocada o inválida. |
| `403` | A la credencial le falta `actions:write`, o la URL apunta a un sitio que la credencial no tiene permitido. Reescribir por `slug` una acción que apunta a un sitio no permitido también da `403`, aunque no cambien la URL. El mensaje dice qué sitio falta. |
| `429` | Pasaron un techo por minuto: 3.000 pedidos por credencial o 200 por dirección IP. Esperen lo que diga `Retry-After`. |

### Por qué reintentar no duplica

Mandar el mismo `slug` es la misma acción, esté activa o dada de baja. Si está activa, se actualiza con lo que venga en el cuerpo y se conserva lo que no venga; si cambian el sistema al que apunta (otro origen), la autorización anterior queda revocada —era para lo que había antes— y la nueva se registra si vino. Si está dada de baja, revive: dar de baja no borra, porque el registro de qué acción se ejecutó contra qué sistema tiene que seguir siendo legible después, así que el `slug` queda ocupado por una fila que el listado no muestra. Un guion de aprovisionamiento que corre dos veces deja una sola acción, con lo último que mandó.

---

## 3. Ver las acciones

```
GET https://api.lelemon.cl/v1/external/actions
```

Alcance: `actions:read`. Devuelve `{ "data": [ ... ] }` con las acciones HTTP que registraron ustedes, con la misma forma que la respuesta del registro. No incluye las acciones nuestras ni las de un servidor MCP: para saber qué puede tener un agente concreto está el paso siguiente.

```
GET https://api.lelemon.cl/v1/external/agents/{agentId}/actions
```

Alcance: `actions:read`. El catálogo **completo** que ese agente puede tener, con `enabled` diciendo cuáles tiene concedidas hoy:

```json
{
  "data": [
    { "slug": "crear_ticket", "name": "Crear un ticket", "description": "...", "source": "Tus sistemas", "enabled": true },
    { "slug": "search_knowledge", "name": "Buscar en el conocimiento", "description": "...", "source": "Lelemon", "enabled": true }
  ]
}
```

`source` es una etiqueta para mostrar, no un código: `Tus sistemas` para lo que registraron ustedes, `Lelemon` para las acciones que traemos nosotros, y el nombre del servidor MCP o del conector cuando viene de ahí. No programen contra ese texto.

Sale el catálogo entero y no sólo lo concedido, porque para prender una hay que saber su `slug`, y un cliente que sólo viera lo suyo concluiría que el resto no se puede conceder. Sin la dirección ni cabeceras. Con `actions:read`, cada entrada trae `destinationOrigin`: el origen al que sale la acción, sea HTTP, de un servidor MCP o de un conector, y `null` si es una herramienta nativa de Lelemon. Está explicado en [la guía para inventariar agentes](lelemon-api-inventario.md).

---

## 4. Conceder y quitar

```
PUT    https://api.lelemon.cl/v1/external/agents/{agentId}/actions/{slug}
DELETE https://api.lelemon.cl/v1/external/agents/{agentId}/actions/{slug}
```

Alcance: `actions:write`. Los dos devuelven `204` sin cuerpo. Los dos son idempotentes: conceder lo que ya estaba, o quitar lo que no estaba, no es un error.

Se concede **de a una, nunca mandando la lista entera**. Si tuvieran que mandar el conjunto completo, para agregar una tendrían que leer las que hay, sumar la suya y mandar todo — y entre el leer y el mandar, lo que alguien concedió desde el panel desaparece sin error y sin rastro. Dos concesiones simultáneas por dos procesos distintos quedan las dos.

### Códigos de estado

| Código | Cuándo |
|---|---|
| `204` | Concedida / quitada. |
| `400` | Sólo en `PUT`: esa acción no existe para su organización, o no alcanza a este agente. Quitar no valida: si una acción dejó de existir hay que poder sacársela igual. |
| `404` | El agente no existe, no es de su organización, o es el asistente interno. |

### Por qué el asistente interno da `404`

El asistente interno de ustedes trae herramientas que leen contactos con su teléfono, tratos y líneas de cartola. Con `actions:write` se podría registrar una acción a una URL propia y concedérsela: no haría falta ni hablarle, el dueño le escribe mañana y los datos salen solos. Por eso la API no lo toca: ni le concede, ni le quita, ni lista lo que tiene. Y el `404` es el mismo que el de un agente que no existe, para no confirmar cuál de los identificadores probados es el interno.

---

## 5. Dar de baja una acción

```
DELETE https://api.lelemon.cl/v1/external/actions/{slug}
```

Alcance: `actions:write`. `204`. Deja de estar disponible y se retira de todos los agentes que la tenían concedida, en el momento. El `slug` queda ocupado: volver a registrarlo revive esta misma acción (ver el punto 2). `404` si no tienen una acción con ese `slug`.

---

## 6. El cuerpo de los errores

Igual que en el resto de la API: `statusCode`, `code`, `message` y, cuando es un `400` de validación, `errors` con el campo que falló y la forma de lo que llegó, nunca su valor.

### Códigos de error

Decidan por `code`, no por el texto del `message`: el texto puede cambiar y el `code` no. Los que importan al registrar una acción:

| `code` | Status | Cuándo pasa | Qué hacer |
|---|---|---|---|
| `validation_failed` | 400 | El cuerpo no cumple lo que pide la ruta. | Corregir el pedido. |
| `action_destination_blocked` | 400 | La dirección no es una URL pública http o https. | Usar una URL pública. |
| `action_origin_not_allowed` | 403 | La credencial no puede crear acciones hacia ese sitio. | Que un administrador agregue el sitio a la credencial en Integraciones, API. |
| `api_key_invalid` | 401 | Falta la credencial, está mal escrita, fue revocada o venció. | Revisar la clave o pedir una nueva. |
| `scope_missing` | 403 | La credencial no tiene el permiso que pide la ruta. | Pedir que le agreguen ese permiso. |
| `rate_limited` | 429 | Pasaron el límite de pedidos. | Esperar lo que diga `Retry-After`. |

Si el error no tiene un código propio, `code` toma uno según el `statusCode` (`not_found` para 404, `conflict` para 409, `internal_error` para 500, y así). La tabla completa está en la guía de agentes, sección 7.

El `400` por versión vieja del texto es el único que conviene manejar por programa, porque va a pasar el día que el texto cambie y le va a pasar a todas las integraciones a la vez:

```json
{
  "statusCode": 400,
  "message": "El texto de autorización cambió: mandaste la versión 2026-08-1 y la vigente es 2026-11-1. Lee el texto nuevo en GET /v1/external/actions/authorization y vuelve a enviarlo con esa versión."
}
```

Lo razonable es que su integración, ante ese mensaje, avise a una persona: aceptar un texto legal nuevo automáticamente es justamente lo que el texto pide que no pase.

---

## 7. Lo que todavía no hacemos

- **Probarla.** El botón de "probar" del panel no tiene equivalente por API.
