# Chat com cliente

Central de Atendimento. Chat ancorado em um pedido, com anexos e SLA de resposta.

> Pré-requisitos: leia primeiro [Comunicação: visão geral](/guia/comunicacao). Toda conversa nasce ancorada num pedido (ULID em `order_id`) — você não vai encontrar uma forma de abrir um chat "solto", sem pedido por trás. Isso é de propósito: amarrar a conversa ao pedido dá contexto imediato (o que foi comprado, o status, o histórico) para você e para o comprador, sem precisar perguntar "de qual pedido você está falando?".

---

## Conceitos

**Participantes:** `customer` (comprador), `store` (sua loja), `platform` (atendimento da plataforma).

**Tipo:** `pre_delivery` (aberta no pagamento do pedido, encerra na entrega) ou `post_delivery` (pós-venda aberto manualmente pelo comprador).

**Status:** `open` ou `closed`. Conversas encerradas trazem `closed_reason`, que pode ser `order_delivered`, `customer_closed`, `admin_closed` ou `auto_inactive`.

**Kind da mensagem:** `user` (humano) ou `system` (evento automático, ex.: "Pedido marcado como entregue").

---

## Listar conversas

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations</span></a>

Ordenada por `last_message_at` desc. Filtros:

| Parâmetro | Para quê |
|-----------|----------|
| `status` | `open` ou `closed`. |
| `type` | `pre_delivery` ou `post_delivery`. |
| `subject_id` | Slug do assunto pós-venda (ex.: `defective_product`, `wrong_item`). |
| `q` | Busca por `order_id` (ULID do pedido). |
| `page`, `per_page` | Paginação (default `per_page=20`). |

---

## Detalhes da conversa

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/{conversation}"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/{conversation}</span></a>

---

## Não lidas

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/unread-count"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/unread-count</span></a>

Soma das mensagens não lidas em todas as conversas abertas. Mensagens enviadas pela própria loja não contam. É a métrica usada nos badges do menu lateral.

---

## Mensagens

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/{conversation}/messages"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/{conversation}/messages</span></a>

Vêm em ordem cronológica (mais antiga primeiro). Cada mensagem tem `kind`:

- `user`: escrita por um participante humano (`customer`, `store` ou `platform`).
- `system`: gerada pela plataforma (ex.: "Pedido marcado como entregue").

O bloco `sender` já vem com `display_name`, `avatar_url`, `avatar_color` e `badge`.

### Polling incremental

Para atualizar a tela sem refazer a paginação, envie `after_message_ulid` com o ULID da última mensagem recebida. Só voltam as posteriores:

```http
GET /v1/seller/conversations/{id}/messages?after_message_ulid=01K8PBMSG00004VWXYZ12345678
```

### Enviar mensagem

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/POST/v1/seller/conversations/{conversation}/messages"><span class="api-method api-method-post">POST</span><span class="api-path">/v1/seller/conversations/{conversation}/messages</span></a>

Pode conter `body` (texto, máx. 5000), `attachments` (até 5 ULIDs já reservados via intent) ou ambos:

```json
{
  "body": "Segue a nota fiscal e a foto da embalagem.",
  "attachments": [
    "01K8PBATT00001VWXYZ12345678",
    "01K8PBATT00002VWXYZ12345678"
  ]
}
```

Erros comuns:

| `message_code` | Quando |
|----------------|--------|
| `CONVERSATION_NOT_PARTICIPANT` (403) | A loja não participa da conversa. |
| `CONVERSATION_CLOSED` (422) | Conversa encerrada. |
| `CONVERSATION_ATTACHMENT_NOT_CONFIRMED` (422) | Algum ULID de anexo ainda não teve `PUT` no storage. |

---

## Marcar como lida

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/POST/v1/seller/conversations/{conversation}/read"><span class="api-method api-method-post">POST</span><span class="api-path">/v1/seller/conversations/{conversation}/read</span></a>

Marca como lidas todas as mensagens até (e incluindo) `last_message_ulid`. Retorna **204**.

```json
{
  "last_message_ulid": "01K8PBMSG00004VWXYZ12345678"
}
```

---

## Anexos

Envio em **3 etapas**: o cliente sobe o arquivo direto no storage (S3/MinIO) por uma URL pré-assinada, e só depois vincula o ULID a uma mensagem. O anexo nasce `RESERVED` e vira `CONFIRMED` automaticamente quando a mensagem é criada (o backend faz a checagem de que o arquivo chegou ao storage).

```mermaid
sequenceDiagram
    autonumber
    participant I as Integrador
    participant A as API
    participant S as Storage S3

    I->>A: POST /attachments/intent<br/>{ original_name, mime_type, size_bytes }
    A-->>I: { attachment_ulid, upload_url, expires_at }
    Note over A: anexo: status=RESERVED

    I->>S: PUT {upload_url}<br/>(binário do arquivo)
    S-->>I: 200 OK

    I->>A: POST /messages<br/>{ body, attachments: [ulid] }
    Note over A: confirma anexo<br/>(checa que existe no S3)<br/>status=CONFIRMED
    A-->>I: { id, ... }
```

### 1. Reservar slot

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/POST/v1/seller/conversations/{conversation}/attachments/intent"><span class="api-method api-method-post">POST</span><span class="api-path">/v1/seller/conversations/{conversation}/attachments/intent</span></a>

Resposta:

```json
{
  "data": {
    "attachment_ulid": "01K8PBATT00001VWXYZ12345678",
    "upload_url": "https://cdn-content.s3.amazonaws.com/.../01K8PBATT00001....jpg?X-Amz-Signature=...",
    "method": "PUT",
    "expires_at": "2026-05-19T11:47:08-03:00"
  }
}
```

### 2. Subir o arquivo

Faça um `PUT {upload_url}` com o binário do arquivo.

### 3. Vincular à mensagem

Chame `POST /messages` enviando `attachments: [attachment_ulid]`.

### Limites

| Limite | Valor |
|--------|-------|
| Tamanho máximo | 25 MB |
| Validade da URL de upload | 15 minutos |
| Anexos por mensagem | 5 |
| Tipos permitidos | `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `application/pdf`, `video/mp4`, `video/webm`, `video/quicktime` |

Erros já na etapa 1:

| `message_code` | Quando |
|----------------|--------|
| `CONVERSATION_ATTACHMENT_TOO_LARGE` (422) | Tamanho acima de 25 MB. |
| `CONVERSATION_ATTACHMENT_MIME_NOT_ALLOWED` (422) | Tipo fora da lista. |

### Baixar

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/{conversation}/attachments/{attachment}/access"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/{conversation}/attachments/{attachment}/access</span></a>

Retorna uma URL temporária assinada pelo storage, válida por **30 minutos**:

```json
{
  "data": {
    "url": "https://cdn-content.s3.amazonaws.com/.../01K8PBATT00001....jpg?X-Amz-Signature=...",
    "expires_at": "2026-05-19T12:02:08-03:00"
  }
}
```

> Faça a requisição direto no `url`. Cada chamada gera uma URL nova; as antigas continuam válidas até expirarem.

---

## SLA de resposta

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/config"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/config</span></a>

Tempo de resposta esperado (em horas), definido pela plataforma. Use para destacar conversas perto do prazo na UI. A mesma rota devolve também `subjects`: o catálogo de assuntos de pós-venda (cada um com `id`, `label` e `applies_to`) — é dele que saem os valores aceitos no filtro `subject_id` da listagem.

```json
{
  "data": {
    "sla_response_hours": 24,
    "subjects": [
      { "id": "defective_product", "label": "Produto com defeito", "applies_to": "post_delivery" },
      { "id": "wrong_item", "label": "Item errado", "applies_to": "post_delivery" }
    ]
  }
}
```

---

## Erros

| `message_code` | HTTP | Quando |
|----------------|------|--------|
| `UNAUTHORIZED` | 401 | Token ausente, inválido ou expirado. |
| `FORBIDDEN` | 403 | Sem permissão. |
| `NOT_FOUND` | 404 | Conversa inexistente. |
| `VALIDATION_ERROR` | 422 | Campos inválidos. |
| `CONVERSATION_NOT_PARTICIPANT` | 403 | Loja não participa da conversa. |
| `CONVERSATION_CLOSED` | 422 | Tentativa de enviar em conversa encerrada. |
| `CONVERSATION_ATTACHMENT_TOO_LARGE` | 422 | Anexo acima de 25 MB. |
| `CONVERSATION_ATTACHMENT_MIME_NOT_ALLOWED` | 422 | Tipo de arquivo fora da lista. |
| `CONVERSATION_ATTACHMENT_NOT_CONFIRMED` | 422 | Anexo não teve `PUT` no storage. |
