> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Guía de integración

> Autenticación, errores, contactos, tickets y webchat en una sola página.

Todo lo que necesitas para integrarte, de principio a fin. Los endpoints en detalle, con un
playground para probarlos, están en la pestaña **Referencia**.

## Autenticación

Cómo obtener y usar la API key de tu organización.

Toda solicitud lleva la API key de tu organización como bearer token:

```http theme={"system"}
Authorization: Bearer rvm_a1b2c3d4e5f6a7b8.9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d
```

### Obtener una key

Las API keys las emite el equipo de Revem para tu organización. Escríbenos a
[soporte@revem.ai](mailto:soporte@revem.ai) indicando para qué sistema es; te entregamos la key
por un canal seguro.

<Warning>
  La key completa se muestra **una sola vez**. Revem la guarda encriptada y no puede recuperarla:
  si se pierde, emitimos una nueva y revocamos la anterior.
</Warning>

### Forma de la key

Una key tiene la forma `rvm_<prefijo>.<secreto>`. El prefijo identifica la key en nuestros
registros y puedes mencionarlo en un correo de soporte sin riesgo; el secreto nunca debe salir de
tu servidor.

### Buenas prácticas

* **Guárdala como secreto** en tu backend (variables de entorno, gestor de secretos). Nunca en el
  código fuente, en un repositorio ni en el navegador.
* **Una key por sistema.** Si tienes más de un servicio que hablan con Revem, pide una key para cada uno:
  revocar una no interrumpe al otro.
* **Rotación.** Pide la nueva key, cámbiala en tu sistema y avísanos para revocar la anterior. La
  key vieja sigue funcionando hasta que se revoca, así que no hay corte.
* **Nunca desde el navegador.** Las rutas de webchat en particular confían en la identidad del
  visitante que tu backend afirma; expuestas al cliente, cualquiera podría escribir como cualquiera
  dentro de tu organización. Ver [Webchat](/guia#webchat).

### Respuestas de autenticación

| Código | Cuándo                                                                                                                    |
| ------ | ------------------------------------------------------------------------------------------------------------------------- |
| `401`  | No hay header `Authorization`, la key no existe o fue revocada.                                                           |
| `403`  | La key es válida pero la ruta no está disponible para API keys. Sólo los endpoints de la pestaña **Referencia** lo están. |

```json 401 theme={"system"}
{ "error": "Unauthorized", "message": "Invalid API key" }
```

```json 403 theme={"system"}
{ "ok": false, "error": "Forbidden", "message": "This endpoint is not available to API keys" }
```

## Errores y límites

Códigos de estado, forma de los errores y límite de solicitudes.

### Códigos de estado

| Código | Significado                                                                                                                      |
| ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Solicitud correcta.                                                                                                              |
| `201`  | Recurso creado (mensajes y adjuntos de webchat).                                                                                 |
| `204`  | Actualización aplicada sin cuerpo de respuesta (ver `PATCH /tickets/public/v1/{id}`).                                            |
| `400`  | El cuerpo o un parámetro no cumple el esquema, o referencia algo que no existe en tu organización.                               |
| `401`  | Falta la key, o no existe o fue revocada.                                                                                        |
| `403`  | La key no puede usar esa ruta o ese recurso.                                                                                     |
| `404`  | El recurso no existe, está en la papelera o pertenece a otra organización. No se distingue cuál.                                 |
| `409`  | Conflicto de identidad: un teléfono o email que ya pertenece a otro contacto.                                                    |
| `422`  | La solicitud es válida en forma pero no en contenido: un atributo personalizado que no existe o cuyo valor no calza con su tipo. |
| `429`  | Superaste el límite de solicitudes por minuto.                                                                                   |
| `5xx`  | Error de Revem. Reintenta con espera exponencial; si persiste, escríbenos.                                                       |

### Forma de los errores

Todo error es un objeto JSON con `ok: false` y una clave `error`. Qué contiene `error` depende del
módulo, así que no lo uses para tomar decisiones sin mirar la tabla:

<Tabs>
  <Tab title="Errores de contactos">
    `error` es la etiqueta del código HTTP y `message` explica el problema. El `422` por atributos
    desconocidos agrega `unknownAttributes` con los nombres que no existen.

    ```json theme={"system"}
    {
      "ok": false,
      "error": "Unprocessable Entity",
      "message": "Atributos sin definición: \"embcodigo\"",
      "unknownAttributes": ["embcodigo"]
    }
    ```
  </Tab>

  <Tab title="Errores de tickets">
    `error` es el mensaje.

    ```json theme={"system"}
    { "ok": false, "error": "Ticket not found" }
    ```
  </Tab>

  <Tab title="Errores de webchat">
    `error` es un **código estable** (`channel_not_found`, `contact_identity_required`,
    `unsupported_media_type`, …) y `message` lo explica. Los códigos posibles de cada endpoint están
    en su página de la referencia.

    ```json theme={"system"}
    {
      "ok": false,
      "error": "contact_identity_required",
      "message": "Para abrir una conversación hace falta un id de contacto, un email o un teléfono."
    }
    ```
  </Tab>
</Tabs>

Los errores de validación (`400`) describen el campo con la notación de la API: `body/phone_number`,
`querystring/limit`, `params/id`.

### Límite de solicitudes

Cada API key puede hacer **600 solicitudes por minuto**, sumando todos los endpoints. Al superarlo,
la API responde `429` y no procesa la solicitud.

```json theme={"system"}
{ "ok": false, "error": "Too many requests. Retry in 1 minute." }
```

Las respuestas incluyen headers para que tu cliente se regule sin llegar al límite:

| Header                  | Contenido                                                   |
| ----------------------- | ----------------------------------------------------------- |
| `x-ratelimit-limit`     | Solicitudes permitidas por minuto.                          |
| `x-ratelimit-remaining` | Las que quedan en la ventana actual.                        |
| `x-ratelimit-reset`     | Segundos hasta que la ventana se reinicia.                  |
| `retry-after`           | Sólo en el `429`: segundos que esperar antes de reintentar. |

<Tip>
  Para una sincronización masiva, respeta `retry-after` y reintenta con espera exponencial. Si
  necesitas un límite mayor de forma sostenida, escríbenos.
</Tip>

### Idempotencia y reintentos

* `GET` y `PATCH` son seguros de reintentar.
* `POST /contacts/public/v1` es un upsert: repetir la misma solicitud deja el mismo resultado.
* `POST /webchat/messages` **no** es idempotente: cada llamada registra un mensaje nuevo. Reintenta
  sólo si no recibiste respuesta, y evita duplicar un mensaje que sí llegó.

## Contactos

Cómo mantener tu base de clientes al día en Revem desde tu CRM, tu tienda o tu ERP.

Los endpoints de contactos viven bajo `/contacts/public/v1`. Un contacto tiene cuatro campos fijos
—`phone_number`, `first_name`, `last_name`, `email`— y cualquier cantidad de **atributos
personalizados**, que van al mismo nivel identificados por su nombre en Revem.

### El teléfono identifica, el id persiste

Hay dos formas de escribir un contacto y conviene entender cuándo usar cada una:

<Steps>
  <Step title="Crear o actualizar por teléfono">
    `POST /contacts/public/v1` recibe el teléfono y decide: si ya hay un contacto con ese número lo
    actualiza, si no lo crea. Es la llamada para una carga inicial y para sistemas que no guardan
    ids de Revem.

    ```bash theme={"system"}
    curl https://api.revem.ai/contacts/public/v1 \
      -H "Authorization: Bearer $REVEM_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "phone_number": "+56912345678",
        "first_name": "Camila",
        "last_name": "Rojas",
        "email": "camila@ejemplo.cl",
        "programa": "Premium",
        "fecha_ultima_compra": "2026-08-30"
      }'
    ```

    La respuesta trae el `id` del contacto y si fue creado (`created: true`) o actualizado.
  </Step>

  <Step title="Guardar el id y actualizar por id">
    Guarda ese `id` junto al cliente en tu sistema. Desde ahí, actualiza con
    `PATCH /contacts/public/v1/{id}`: acepta los mismos campos y aquí el teléfono es un campo más,
    así que **un cliente que cambia de número sigue siendo el mismo contacto**.

    ```bash theme={"system"}
    curl -X PATCH https://api.revem.ai/contacts/public/v1/3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90 \
      -H "Authorization: Bearer $REVEM_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "phone_number": "+56998765432" }'
    ```

    Con el upsert, ese mismo cambio habría creado un segundo contacto con la mitad del historial.
  </Step>
</Steps>

### Reglas que conviene saber

<AccordionGroup>
  <Accordion title="Una clave ausente no borra nada">
    Varios sistemas escriben sobre el mismo contacto (tu integración, el equipo desde la app, el
    agente). Por eso una solicitud parcial es una actualización parcial: sólo cambian las claves
    que envías. Enviar `{"first_name": "Camila"}` corrige el nombre y deja el apellido como estaba.

    Por la misma razón esta API no vacía campos: un email o un apellido en blanco se trata como
    ausente. Limpiar un dato es una acción deliberada desde la app.
  </Accordion>

  <Accordion title="Los atributos personalizados se identifican por nombre">
    Cualquier clave del cuerpo que no sea uno de los cuatro campos fijos se interpreta como un
    atributo personalizado y se busca por nombre entre los definidos para contactos en tu
    organización, sin distinguir mayúsculas.

    * Un nombre que no existe responde `422` con la lista en `unknownAttributes`. La API nunca crea
      definiciones por su cuenta: un error de tipeo sería un campo permanente.
    * Un valor que no calza con el tipo declarado (texto en un atributo numérico, por ejemplo)
      también responde `422`.
    * Los valores pueden venir como texto aunque el atributo sea numérico o booleano; Revem los
      convierte según la definición.
    * Hasta 100 atributos por solicitud.

    Los nombres `id`, `created_at` y `updated_at` están reservados para la lectura: enviarlos en un
    cuerpo responde `422`.
  </Accordion>

  <Accordion title="El email es dato, no identidad">
    Si el email que envías ya pertenece a otro contacto, la solicitud **sí se aplica** —nombre,
    apellido y atributos se guardan— y el email queda sin escribir, con un aviso en `warnings`.
    Revisa ese arreglo en tu integración: una respuesta `200` con avisos es información, no ruido.

    ```json theme={"system"}
    {
      "ok": true,
      "id": "3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90",
      "created": false,
      "warnings": ["El email \"camila@ejemplo.cl\" ya pertenece a otro contacto, así que no se escribió"]
    }
    ```
  </Accordion>

  <Accordion title="Un contacto en la papelera sigue siendo dueño de su número">
    Si el teléfono pertenece a un contacto que el equipo envió a la papelera, el upsert responde
    `409` en vez de resucitarlo en silencio. Restaurarlo o eliminarlo definitivamente se hace desde
    Contactos en la app. `PATCH /{id}` sobre ese contacto responde `404`, igual que `GET /{id}`: la
    API key no ve la papelera.
  </Accordion>

  <Accordion title="Formato del teléfono">
    Recomendamos E.164 (`+56912345678`). Revem normaliza las grafías habituales al guardar y al
    buscar, así que el número con que creaste un contacto también lo encuentra en `?phone=`. Un
    número que no se puede normalizar se guarda tal cual.
  </Accordion>
</AccordionGroup>

### Leer contactos

`GET /contacts/public/v1/{id}` devuelve un contacto en la misma forma plana que acepta la
escritura, con sus atributos personalizados por nombre. Un campo sin valor se **omite**; la
excepción es `phone_number`, que siempre viene y puede ser `null` para un contacto que llegó por un
canal sin teléfono.

```json theme={"system"}
{
  "id": "3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90",
  "phone_number": "+56912345678",
  "first_name": "Camila",
  "last_name": "Rojas",
  "email": "camila@ejemplo.cl",
  "created_at": "2026-08-12T14:03:22.000Z",
  "updated_at": "2026-09-01T09:41:05.000Z",
  "programa": "Premium",
  "fecha_ultima_compra": "2026-08-30"
}
```

`GET /contacts/public/v1` lista los contactos del más reciente al más antiguo según su fecha de
creación, paginados con `limit` (hasta 200) y `offset`, y acepta filtros por `phone`, `email` y
`updated_since`. La respuesta trae `has_more` en lugar de un total.

### Sincronización incremental

Para traer a tu sistema lo que cambió en Revem, recorre la lista con `updated_since`:

```bash theme={"system"}
curl "https://api.revem.ai/contacts/public/v1?updated_since=2026-09-01T00:00:00Z&limit=200&offset=0" \
  -H "Authorization: Bearer $REVEM_API_KEY"
```

<Warning>
  `updated_since` **filtra pero no ordena**: las páginas van por fecha de creación, así que un
  contacto editado mientras recorres las páginas puede quedar fuera de la que le correspondía.
  Solapa la ventana unos minutos con la corrida anterior en vez de empezar exactamente donde
  terminó la última; el upsert en tu lado hace que repetir un contacto sea inocuo.
</Warning>

### Lo que la API no hace

* **No elimina contactos.** Enviar un contacto a la papelera es una acción deliberada desde la app.
* **No une contactos duplicados.** Dos contactos partidos por un cambio de número antes de usar
  `PATCH /{id}` se unen desde Contactos.
* **No expone el nombre compuesto.** Publica `first_name` y `last_name` tal como están guardados;
  compón el nombre completo en tu lado si lo necesitas.

## Tickets

Cómo leer y actualizar los tickets de tu embudo desde un sistema externo.

Los endpoints de tickets viven bajo `/tickets/public/v1`. Un ticket es una oportunidad o un caso
dentro de un embudo: tiene una etapa, un estado, un responsable, contactos vinculados y atributos
personalizados.

<Info>
  Una API key ve **todos** los tickets de la organización. Las reglas de visibilidad por dueño o
  por grupo aplican a las personas del equipo dentro de la app, no a las integraciones.
</Info>

### Listar y filtrar

`GET /tickets/public/v1` pagina con `limit` (hasta 200) y `offset`, y acepta filtros combinables:

| Parámetro             | Qué filtra                                                                                     |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| `status`              | `active`, `postponed`, `won` o `lost`                                                          |
| `ids`                 | Una lista de ids separados por coma, hasta 200 — útil para refrescar lo que tienes en pantalla |
| `contactId`           | Tickets donde ese contacto es el contacto principal o la cuenta                                |
| `search`              | Texto contenido en el título                                                                   |
| `funnelStageId`       | Tickets en una etapa del embudo                                                                |
| `assignedToProfileId` | Tickets asignados a un perfil                                                                  |

```bash theme={"system"}
curl "https://api.revem.ai/tickets/public/v1?status=active&funnelStageId=12&limit=50" \
  -H "Authorization: Bearer $REVEM_API_KEY"
```

La respuesta trae `tickets` y `hasMore`.

### Leer un ticket

```bash theme={"system"}
curl https://api.revem.ai/tickets/public/v1/c0a8012e-5b3d-4e7f-9a1b-2c3d4e5f6a7b \
  -H "Authorization: Bearer $REVEM_API_KEY"
```

```json theme={"system"}
{
  "id": "c0a8012e-5b3d-4e7f-9a1b-2c3d4e5f6a7b",
  "title": "Cotización plan anual",
  "status": "active",
  "funnelStageId": 12,
  "funnelId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
  "assignedToProfileId": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "createdByProfileId": null,
  "pointOfContact": "3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90",
  "account": null,
  "customAttributes": { "origen": "web" },
  "lossReason": null,
  "visibility": "TENANT",
  "visibilityGroupId": null,
  "postponedUntil": null,
  "closedAt": null,
  "createdAt": "2026-09-02T12:00:00.000Z",
  "updatedAt": "2026-09-03T08:15:30.000Z",
  "conversations": [{ "id": "a1d2c3b4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "channelType": "Webchat" }]
}
```

`pointOfContact` y `account` son ids de contacto: puedes resolverlos con
`GET /contacts/public/v1/{id}`. `conversations` es una lista porque un ticket puede tener más de
una conversación (por ejemplo una de WhatsApp y una de webchat).

### Actualizar un ticket

`PATCH /tickets/public/v1/{id}` recibe sólo los campos que quieres cambiar:

```bash theme={"system"}
curl -X PATCH https://api.revem.ai/tickets/public/v1/c0a8012e-5b3d-4e7f-9a1b-2c3d4e5f6a7b \
  -H "Authorization: Bearer $REVEM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "won", "funnelStageId": 14 }'
```

* `title`, `funnelStageId`, `status`, `assignedToProfileId`, `pointOfContact`, `account`,
  `lossReason` y `customAttributes` son editables. Los ids de contactos, perfiles y etapas deben
  pertenecer a tu organización; si no, la respuesta es `400`.
* `assignedToProfileId`, `pointOfContact`, `account` y `lossReason` aceptan `null` para vaciarlos.
* `customAttributes` **reemplaza** el conjunto completo: envía todos los que quieras conservar.
* La respuesta normal es `200` con el ticket actualizado. Un `204` significa que el cambio se
  aplicó pero quien llama ya no puede leer el resultado; con una API key no ocurre en la práctica.

<Warning>
  `status` acepta `active`, `won` y `lost`, pero **no** `postponed`. Posponer un ticket programa su
  reapertura automática, y eso sólo se hace desde la app.
</Warning>

### Lo que la API no hace

* **No crea tickets.** Un ticket nace de una conversación con un contacto, para que conserve ese
  contexto. Si necesitas abrir uno desde tu sistema, el camino es una conversación (por ejemplo un
  mensaje de [webchat](/guia#webchat) con `createTicketOnConversation` activo en el canal).
* **No elimina tickets.** Se conservan para auditoría.
* **No lee los mensajes de las conversaciones de un ticket**, salvo las de webchat con
  `GET /webchat/conversations/{id}/messages`.

## Webchat

Cómo registrar los mensajes del chat de tu sitio en Revem y mostrar las respuestas del equipo o del agente de IA.

El canal **Webchat** conecta un chat que tú controlas —el widget de tu sitio, tu app— con la
bandeja de entrada de Revem. Tu backend registra lo que escribe el visitante; el equipo o el agente
de IA responden desde Revem; tu backend lee esas respuestas y las muestra.

### Antes de empezar

1. En Revem, crea un canal de tipo Webchat en **Canales** y configúralo: etapa del embudo, modo de
   conversación (autónomo, asistido o manual) y si cada conversación abre un ticket.
2. Toma el **id del canal** desde esa pantalla. Es opcional en las llamadas si tienes un solo
   webchat; si tienes varios, envíalo siempre.
3. Pide tu [API key](/guia#autenticación).

<Warning>
  Estas rutas se llaman desde **tu servidor**, nunca desde el navegador. Revem confía en la
  identidad del visitante que tu backend afirma en `contact`: el contacto resuelto recibe el
  historial, los tickets y el contexto que el agente usa para responder. Autentica a tu usuario
  antes de llamar, y no expongas la API key ni estas rutas al cliente.
</Warning>

### El flujo

<Steps>
  <Step title="El visitante escribe: registra el primer mensaje">
    Sin `conversationId`, `POST /webchat/messages` abre una conversación. Indica quién escribe en
    `contact` con al menos una identidad —`id`, `email` o `phone`— y opcionalmente su `name`.

    ```bash theme={"system"}
    curl https://api.revem.ai/webchat/messages \
      -H "Authorization: Bearer $REVEM_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "channelId": "b7e3d2a1-4c5f-4e6d-9a8b-7c6d5e4f3a2b",
        "contact": { "email": "camila@ejemplo.cl", "name": "Camila Rojas" },
        "body": "Hola, ¿tienen horas disponibles esta semana?"
      }'
    ```

    ```json theme={"system"}
    { "conversationId": "a1d2c3b4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "messageId": 482 }
    ```

    Guarda el `conversationId` asociado a la sesión del visitante.
  </Step>

  <Step title="Mensajes siguientes: envía el conversationId">
    A partir del segundo mensaje envía sólo `conversationId` y el contenido. `channelId` y
    `contact` se ignoran cuando la conversación ya existe.

    ```bash theme={"system"}
    curl https://api.revem.ai/webchat/messages \
      -H "Authorization: Bearer $REVEM_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "conversationId": "a1d2c3b4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "body": "El jueves por la tarde me acomoda."
      }'
    ```
  </Step>

  <Step title="Lee las respuestas">
    Las respuestas del equipo o del agente se generan de forma **asíncrona**. Léelas con
    `GET /webchat/conversations/{id}/messages`, que devuelve los mensajes del más reciente al más
    antiguo. Los que escribió el visitante vienen con `origin: "INBOUND"`; las respuestas, con
    `origin: "OUTBOUND"`.

    ```bash theme={"system"}
    curl "https://api.revem.ai/webchat/conversations/a1d2c3b4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/messages?limit=20" \
      -H "Authorization: Bearer $REVEM_API_KEY"
    ```

    ```json theme={"system"}
    {
      "data": [
        {
          "id": 483,
          "body": "¡Hola Camila! Sí, tenemos disponibilidad el jueves y el viernes por la tarde.",
          "attachments": [],
          "origin": "OUTBOUND",
          "status": "sent",
          "createdAt": "2026-09-05T15:20:19.000Z"
        },
        {
          "id": 482,
          "body": "Hola, ¿tienen horas disponibles esta semana?",
          "attachments": [],
          "origin": "INBOUND",
          "status": "sent",
          "createdAt": "2026-09-05T15:20:11.000Z"
        }
      ],
      "nextCursor": null
    }
    ```

    Mientras la conversación está abierta en el widget, consulta esta ruta periódicamente (cada
    pocos segundos) y muestra los mensajes con `id` mayor al último que ya mostraste. Para
    historial más antiguo, pasa `nextCursor` en `cursor`.
  </Step>
</Steps>

### Identificar al visitante

`contact` acepta tres identidades y se prueban **en este orden**, de la más firme a la menos:

| Campo   | Comportamiento                                                                                                                                                                                                                                       |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`    | El id de contacto de Revem (el de `/contacts/public/v1`). Cuando viene, manda: no se cae a las otras dos y **no crea contactos**; un id que no existe responde `404`. Úsalo cuando tu usuario ya está en Revem: es la única identidad que no cambia. |
| `email` | Busca un contacto con ese email (sin distinguir mayúsculas) o lo crea.                                                                                                                                                                               |
| `phone` | Busca un contacto con ese teléfono, aceptando las grafías habituales, o lo crea.                                                                                                                                                                     |

Un visitante anónimo necesita al menos una identidad: sin ninguna, la respuesta es `400`
`contact_identity_required`. Si tu chat permite escribir sin identificarse, pide el email o el
teléfono antes del primer mensaje, o genera un identificador propio y guárdalo como atributo del
contacto por la [API de contactos](/guia#contactos).

`name` es opcional y se usa al crear el contacto o al titular el ticket; no sobrescribe el nombre
de un contacto existente.

### Adjuntos

Un mensaje puede llevar texto, adjuntos o ambos, con hasta 20 adjuntos y 4000 caracteres.

<Steps>
  <Step title="Sube el archivo">
    `POST /webchat/media` recibe `multipart/form-data` con un archivo y devuelve su `mediaId`.

    ```bash theme={"system"}
    curl https://api.revem.ai/webchat/media \
      -H "Authorization: Bearer $REVEM_API_KEY" \
      -F "file=@radiografia.pdf;type=application/pdf"
    ```

    ```json theme={"system"}
    { "mediaId": "d4c3b2a1-6f5e-4d7c-8b9a-0f1e2d3c4b5a" }
    ```

    El formato se valida por el `Content-Type` de la parte. Imágenes PNG y JPEG hasta 5 MB; video
    MP4 y 3GP hasta 16 MB; PDF, Word, Excel y texto plano hasta 25 MB.
  </Step>

  <Step title="Referencia el adjunto en el mensaje">
    ```bash theme={"system"}
    curl https://api.revem.ai/webchat/messages \
      -H "Authorization: Bearer $REVEM_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "conversationId": "a1d2c3b4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "body": "Le adjunto la radiografía.",
        "attachments": [{ "mediaId": "d4c3b2a1-6f5e-4d7c-8b9a-0f1e2d3c4b5a" }]
      }'
    ```
  </Step>
</Steps>

Al leer mensajes, cada adjunto viene con una `url` **firmada que expira en una hora**, además de
`fileName` y `mimeType`. No guardes esas URLs: vuelve a pedir la página cuando las necesites.

### Qué pasa en Revem con cada mensaje

* Un contacto nuevo nace **confirmado** (visible en Contactos y elegible para tickets y campañas)
  si el canal tiene activo *crear contacto al recibir*, que es el valor por defecto.
* Si el canal tiene activo *crear ticket por conversación*, cada conversación tiene un ticket en el
  embudo configurado, y se vuelve a abrir uno si el anterior se cerró.
* En modo **autónomo** responde el agente de IA del canal; en **asistido** o **manual** responde el
  equipo desde la bandeja de entrada. El agente puede escalar a manual; tu integración no necesita
  distinguirlo, sólo leer los mensajes `OUTBOUND`.

### Lo que la API no hace todavía

* **No hay webhook de salida.** Hoy las respuestas se leen consultando la conversación. Si tu caso
  necesita entrega inmediata en el servidor, escríbenos.
* **El agente no envía adjuntos** por este canal; sólo el equipo.
* **No crea canales.** Los webchats se crean y configuran desde la app.
