# Publicar mensajes en un canal

Referencia de POST /api/external/v1/chat/channels/:channelId/messages: publica un mensaje de texto con la identidad de tu aplicación.

Con este endpoint tu aplicación publica un mensaje de texto en un canal del Chat, firmado con su nombre y su logotipo. El mensaje aparece al momento a las personas del canal, como cualquier otro, y les llega como aviso.

Antes de publicar, UTILIA OS comprueba las dos puertas de [Scopes y acceso por canal](https://os.utilia.ai/docs/developers/chat-platform/scopes-and-access): el scope `chat:messages:send` y una concesión activa en el canal que incluya «Escribir mensajes».

**El viaje de un mensaje**

Participantes: Tu servidor, UTILIA OS y Personas del canal.

1. Tu servidor → UTILIA OS: `POST /api/external/v1/chat/channels/:channelId/messages`. Con X-Api-Key y el texto en content.
2. UTILIA OS (por su cuenta): Comprueba el plan, el scope, la concesión del canal y el límite de 600 mensajes por hora.
3. UTILIA OS → Tu servidor (respuesta): 201: el mensaje publicado. Con author.kind APP y el nombre de tu aplicación.
4. UTILIA OS → Personas del canal: Mensaje en el canal. Aparece al momento, firmado por tu aplicación, y llega como aviso.

**`POST /api/external/v1/chat/channels/:channelId/messages`**

Publica un mensaje como la aplicación en un canal concedido

Parámetro de ruta: `channelId`.

## Petición

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `channelId` | `string (ruta)` | Sí | Identificador del canal donde se publica. |
| `content` | `string` | Sí | Texto del mensaje. Hasta 4.000 caracteres; no puede quedar vacío tras quitar los espacios. |
| `parentMessageId` | `string` | No | Mensaje al que responde, para publicar dentro de un hilo. |
| `metadata` | `object` | No | Datos libres de tu aplicación (nunca secretos). Se guardan con el mensaje. |

El cuerpo no acepta organización ni autor: la organización sale de la clave de API y el autor es siempre tu aplicación.

## Respuesta (201)

| Nombre | Tipo | Descripción |
| - | - | - |
| `id` | `string` | Identificador del mensaje. |
| `channelId` | `string` | Canal donde se publicó. |
| `content` | `string \| null` | Texto del mensaje. |
| `type` | `string` | Tipo de mensaje; por esta vía, siempre TEXT. |
| `author` | `object` | Autor: { kind, id, displayName }. Por esta vía, kind es siempre APP y displayName es el nombre de tu aplicación. |
| `parentMessageId` | `string \| null` | Mensaje al que responde, si va en un hilo. |
| `mentions` | `string[]` | Menciones; por esta vía, siempre vacío. |
| `createdAt` | `string` | Fecha de creación (ISO 8601). |
| `editedAt` | `string \| null` | Fecha de la última edición, o null. |
| `isDeleted` | `boolean` | Si el mensaje está eliminado. |

## Límite

Cada aplicación puede publicar hasta **600 mensajes por hora** por esta vía. Al superarlo, la respuesta es 429 `RATE_LIMIT_EXCEEDED` hasta que pasa la hora. Este límite se suma al general de la clave de API.

## Errores

| Código | `errorCode` | Cuándo |
| - | - | - |
| 400 | `VALIDATION_ERROR` | Falta `content` o pasa de 4.000 caracteres. |
| 401 | | La clave de API falta o no es válida. |
| 403 | `MODULE_NOT_ENABLED` | El plan de tu organización no incluye el Chat. |
| 403 | `INSUFFICIENT_SCOPE` | La aplicación no tiene el scope `chat:messages:send`. |
| 403 | `APP_NOT_AUTHORIZED_IN_CHANNEL` | No hay concesión activa con «Escribir mensajes», la aplicación ya no es miembro, o no es un canal ni un grupo. |
| 404 | `CHANNEL_NOT_FOUND` | El canal no existe o es de otra organización. |
| 422 | `VALIDATION_ERROR` | El texto queda vacío al quitar los espacios. |
| 429 | `RATE_LIMIT_EXCEEDED` | Se superaron los 600 mensajes por hora. |

## Ejemplos

### curl

```bash
curl -X POST 'https://os.utilia.ai/api/external/v1/chat/channels/cnv_8f3a21/messages' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_tu_clave_de_api' \
  -d '{
    "content": "Lectura de las 09:00 — Temperatura de la sala de servidores: 21,4 °C"
  }'
```

### fetch

```javascript
const response = await fetch(
  'https://os.utilia.ai/api/external/v1/chat/channels/cnv_8f3a21/messages',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'utl_tu_clave_de_api',
    },
    body: JSON.stringify({
      content:
        'Lectura de las 09:00 — Temperatura de la sala de servidores: 21,4 °C',
    }),
  },
);

const message = await response.json();
console.log(message.id, message.author.displayName);
```

### Python

```python
import requests

respuesta = requests.post(
    "https://os.utilia.ai/api/external/v1/chat/channels/cnv_8f3a21/messages",
    headers={
        "Content-Type": "application/json",
        "X-Api-Key": "utl_tu_clave_de_api",
    },
    json={
        "content": "Lectura de las 09:00 — Temperatura de la sala de servidores: 21,4 °C",
    },
)

mensaje = respuesta.json()
print(mensaje["id"], mensaje["author"]["displayName"])
```

Respuesta:

```json
{
  "id": "msg_a1b2c3d4",
  "channelId": "cnv_8f3a21",
  "content": "Lectura de las 09:00 — Temperatura de la sala de servidores: 21,4 °C",
  "type": "TEXT",
  "author": {
    "kind": "APP",
    "id": "app_termometro",
    "displayName": "Termómetro"
  },
  "parentMessageId": null,
  "mentions": [],
  "createdAt": "2026-07-12T09:00:03.512Z",
  "editedAt": null,
  "isDeleted": false
}
```

## Siguiente paso

Para que tu aplicación se entere de las respuestas del equipo y conteste, suscríbela a los [webhooks de chat](https://os.utilia.ai/docs/developers/chat-platform/webhooks). El [ejemplo del termómetro](https://os.utilia.ai/docs/developers/chat-platform/example-thermometer) lo junta todo.
