# Mensagens

> Enviar texto, mídia e modelos; ver status.

Envie texto, mídia (imagem, vídeo, áudio, documento) ou modelo aprovado. O envio vai para a fila e responde `202` na hora; o status (pendente, enviada, entregue, lida, falhou) chega pelo webhook `mensagem.status` ou por [`GET /mensagens/{id}`](https://docs.forsi.com.br/api/mensagens/ver-mensagem). Para consultar mensagens de toda a organização use [`GET /mensagens`](https://docs.forsi.com.br/api/mensagens/listar-mensagens-org); também é possível listar mensagens de uma conversa.

> **Figura: O caminho de uma mensagem enviada.** O seu sistema chama POST /mensagens. A API responde 202 na hora e põe a mensagem na fila (status pendente). A FORSI entrega à Meta (enviada); a Meta avisa quando chega ao aparelho (entregue) e quando a pessoa lê (lida). Se algo der errado, o status vira falhou, com o motivo. Cada mudança chega ao seu webhook como o evento mensagem.status; também dá para consultar com GET /mensagens/{id}.

- Para quem: `conversa_id` (responder numa conversa que já existe) **ou** `telefone` (com DDI e DDD). Só um dos dois.
- Pelo telefone, a conversa e o contato são criados se ainda não existirem; `nome` dá nome ao contato novo.
- Pelo telefone, com mais de um WhatsApp conectado, mande `caixa_id` (veja [`GET /me`](https://docs.forsi.com.br/api/conta/me)), senão vem `422 caixa_obrigatoria`. Com `conversa_id` a mensagem sai pela caixa da conversa (`caixa_id` e `nome` só valem com telefone).
- `responde_a_id` cita uma mensagem da mesma conversa.
- Na API oficial do WhatsApp, fora da janela de 24 horas desde a última mensagem do contato, só sai **modelo aprovado** (`422 janela_fechada`). Veja os modelos em [`GET /modelos`](https://docs.forsi.com.br/api/modelos/listar-modelos).
- Mídia vai por link https público (`midia.url`): o WhatsApp baixa dele na hora do envio. O `texto` vira a legenda.
- Mande `Idempotency-Key` para poder repetir sem mandar duas vezes (veja [Idempotência](https://docs.forsi.com.br/idempotencia)).
- Limite próprio: 60 envios por minuto por chave e 120 por organização.
- `canal` (opcional, só WhatsApp): `auto` (padrão, igual a omitir) segue a regra da caixa; `oficial` sai só pela API oficial da Meta; `qr` sai só pelo WhatsApp por QR. A escolha manual nunca troca de canal: se o canal escolhido não puder enviar, vem erro (`422 canal_oficial_indisponivel`, `canal_oficial_sem_grupo`, `canal_qr_indisponivel`, `canal_qr_sem_modelo`, `canal_so_whatsapp`; a janela de 24 h vale em `oficial`) ou, se cair depois de aceito, a mensagem fica `falhou` com o código em `erro`. Modelo só sai pela oficial. Repetir o mesmo `id_cliente` com outro `canal` dá `409 idempotencia_conflito`.

## Baixar mídia recebida

Cada item de `midia[]` nas mensagens e nos webhooks `mensagem.recebida` / `mensagem.enviada` traz `id` estável, além de `mime`, `nome_arquivo`, `tamanho` e `duracao_s` quando disponíveis. Baixe os bytes por [`GET /mensagens/{id}/midia`](https://docs.forsi.com.br/api/mensagens/baixar-midia), autenticado com escopo `mensagens:ler`. Use `?item=<id-do-anexo>` para escolher um anexo por esse id ou `?item=0` para escolher pelo índice começando em zero; sem `item`, baixa o primeiro.

- A resposta `200` é o arquivo binário, com `Content-Type` e `Content-Disposition`. O endpoint aceita `Range: bytes=...` e responde `206` com `Content-Range`.
- Se o download sob demanda estiver em andamento, recebe `202` com `erro.codigo: midia_baixando` e `Retry-After` (aguarde esse número de segundos antes de tentar de novo). Se não for possível obter o arquivo, recebe `409 midia_indisponivel`.
- Mensagem inexistente, de outra organização ou sem mídia responde `404`.

```json title="Responder numa conversa que já existe"
{
  "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129",
  "tipo": "texto",
  "texto": "Perfeito, remarquei para quinta às 10h."
}
```

```json title="Imagem com legenda"
{
  "telefone": "+55 31 98888-7777",
  "tipo": "imagem",
  "midia": {"url":"https://seu-sistema.com.br/arquivos/orcamento.jpg"},
  "texto": "Segue o orçamento."
}
```

```json title="Documento (PDF)"
{
  "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129",
  "tipo": "documento",
  "midia": {
    "url": "https://seu-sistema.com.br/arquivos/orcamento.pdf",
    "nome_arquivo": "Orçamento.pdf"
  }
}
```

```json title="Modelo aprovado (abre a conversa fora da janela de 24 h)"
{
  "telefone": "+55 31 98888-7777",
  "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445",
  "tipo": "modelo",
  "modelo": {"nome":"lembrete_consulta","idioma":"pt_BR","corpo":["Maria","9h"]}
}
```

```json title="Escolher o canal (só pela API oficial, sem cair para o QR)"
{
  "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129",
  "tipo": "texto",
  "texto": "Seu pedido saiu para entrega.",
  "canal": "oficial"
}
```

**Códigos de erro comuns aqui**: `tipo_invalido`, `texto_vazio`, `texto_longo`, `midia_invalida`, `midia_indisponivel`, `midia_baixando`, `janela_fechada`, `conta_restrita`, `caixa_obrigatoria`, `caixa_sem_conexao`, `caixa_inexistente`, `canal_sem_iniciar`, `modelo_sem_api_oficial`, `modelo_inexistente`, `telefone_invalido`, `conversa_inexistente`, `mensagem_inexistente`, `canal_oficial_indisponivel`, `canal_oficial_sem_grupo`, `canal_qr_indisponivel`, `canal_qr_sem_modelo`, `canal_so_whatsapp` (todos em [Erros](https://docs.forsi.com.br/erros)).

## POST /mensagens — Enviar mensagem

- URL: `POST https://chat.forsi.com.br/api/v1/mensagens`
- Acesso: escopo `mensagens:enviar` · módulo do plano: API · aceita `Idempotency-Key`
- Sucesso: `202`, `{"dados": ...}` com `EnvioAceito`
- Página: https://docs.forsi.com.br/api/mensagens/enviar-mensagem (Markdown: https://docs.forsi.com.br/api/mensagens/enviar-mensagem.md)

Texto, mídia (imagem, vídeo, áudio, documento) ou modelo aprovado. Vai para a fila de envio e responde 202 na hora; acompanhe pelo webhook `mensagem.status` ou por GET /mensagens/{id}. Na API oficial, fora da janela de 24 h só modelo. Envie `Idempotency-Key` ou `id_cliente` (UUID) para identificar o envio.

**Corpo (JSON)**

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `id_cliente` | uuid | não | UUID estável do envio; substitui Idempotency-Key. |
| `conversa_id` | uuid | não | Responder numa conversa que já existe. Ou use telefone. |
| `telefone` | string | não | Com DDI e DDD. Acha o contato com ou sem o 9º dígito. mín. 8 caracteres até 30 caracteres ex.: +55 31 98888-7777 |
| `caixa_id` | uuid | não | Obrigatório quando a organização tem mais de um WhatsApp conectado. |
| `nome` | string | não | Nome do contato, se ele ainda não tiver. até 120 caracteres |
| `tipo` | "texto" \| "imagem" \| "video" \| "audio" \| "documento" \| "modelo" | não | padrão "texto" |
| `texto` | string | não | Texto (ou legenda da mídia). até 4096 caracteres |
| `midia` | object | não |  |
| `midia.url` | string | sim | Link público https do arquivo. O WhatsApp baixa dele na hora do envio. até 2000 caracteres |
| `midia.mime` | string | não | até 100 caracteres |
| `midia.nome_arquivo` | string | não | Nome que o contato vê (documentos). até 200 caracteres |
| `modelo` | object | não |  |
| `modelo.id` | uuid | não | Id do modelo (GET /modelos). Ou use nome + idioma. |
| `modelo.nome` | string | não | até 512 caracteres |
| `modelo.idioma` | string | não | até 15 caracteres ex.: pt_BR |
| `modelo.corpo` | lista de string ou objeto livre | não | Valores das variáveis do corpo: lista na ordem ({{1}}, {{2}}...) ou objeto por nome. |
| `modelo.cabecalho` | object | não |  |
| `modelo.botoes` | objeto livre | não | Final do link (ou código) por índice do botão: {"0": "abc123"}. |
| `responde_a_id` | uuid | não | Id de uma mensagem desta conversa para citar. |
| `canal` | "auto" \| "oficial" \| "qr" | não | Opcional, só WhatsApp. "auto" (padrão, igual a omitir) segue a regra da caixa; "oficial" sai só pela API oficial da Meta; "qr" sai só pelo WhatsApp por QR. Manual nunca troca de canal: se o escolhido não puder enviar, dá erro. |

**Exemplo (curl)**

```bash
curl -X POST "https://chat.forsi.com.br/api/v1/mensagens" \
  -H "Authorization: Bearer $FORSI_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-4821" \
  -d '{
    "telefone": "+55 31 98888-7777",
    "tipo": "texto",
    "texto": "Olá! Sua consulta está confirmada para amanhã às 9h."
  }'
```

**Resposta `202` (exemplo)**

```json
{
  "dados": {
    "mensagem_id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629",
    "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129",
    "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39",
    "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445",
    "conversa_nova": true,
    "estado": "pendente"
  }
}
```

**Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela).

## GET /mensagens — Listar mensagens

- URL: `GET https://chat.forsi.com.br/api/v1/mensagens`
- Acesso: escopo `mensagens:ler` · módulo do plano: API
- Sucesso: `200`, `{"dados": ...}` com lista de `Mensagem` (paginada por cursor)
- Página: https://docs.forsi.com.br/api/mensagens/listar-mensagens-org (Markdown: https://docs.forsi.com.br/api/mensagens/listar-mensagens-org.md)

Mensagens da organização, da mais nova para a mais antiga. Notas internas não aparecem. Filtre por conversa_id ou caixa_id.

**Parâmetros de consulta (query string)**

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `limite` | integer | não | Itens por página (1 a 100, padrão 50). |
| `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres |
| `conversa_id` | uuid | não |  |
| `caixa_id` | uuid | não |  |

**Exemplo (curl)**

```bash
curl "https://chat.forsi.com.br/api/v1/mensagens" \
  -H "Authorization: Bearer $FORSI_CHAVE"
```

**Resposta `200` (exemplo)**

```json
{
  "dados": [
    {
      "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629",
      "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129",
      "direcao": "entrada",
      "tipo": "texto",
      "texto": "Oi, queria remarcar minha consulta",
      "estado": "pendente",
      "erro": "Connection timed out",
      "autor_tipo": "contato",
      "responde_a_id": "ad52f13b-411d-4526-9d07-91f222c74891",
      "id_whatsapp": "wamid.HBgNNTUzMTk4ODg4Nzc3NxUCABEYEjQ",
      "enviada_em": "2026-10-04T13:05:12.481Z",
      "criado_em": "2026-10-04T13:05:12.481Z",
      "editada_em": "2026-10-04T13:05:12.481Z",
      "apagada_em": "2026-10-04T13:05:12.481Z",
      "pela_api": {"chave_id":"71c5c606-a9d7-4270-8891-45d783aa9b71"},
      "modelo": "texto",
      "midia": [
        {
          "id": "94ac725d-5a70-40b2-8fa6-cdd23a32ac85",
          "mime": "image/jpeg",
          "nome_arquivo": "foto.jpg",
          "tamanho": 48213,
          "duracao_s": 1.5
        }
      ]
    }
  ],
  "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5"
}
```

**Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela).

## GET /mensagens/{id} — Ver uma mensagem

- URL: `GET https://chat.forsi.com.br/api/v1/mensagens/{id}`
- Acesso: escopo `mensagens:ler` · módulo do plano: API
- Sucesso: `200`, `{"dados": ...}` com `Mensagem`
- Página: https://docs.forsi.com.br/api/mensagens/ver-mensagem (Markdown: https://docs.forsi.com.br/api/mensagens/ver-mensagem.md)

Inclusive o status atual (pendente, enviada, entregue, lida, falhou).

**Parâmetros no caminho**

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `id` | uuid | sim | No caminho da URL. |

**Exemplo (curl)**

```bash
curl "https://chat.forsi.com.br/api/v1/mensagens/4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629" \
  -H "Authorization: Bearer $FORSI_CHAVE"
```

**Resposta `200` (exemplo)**

```json
{
  "dados": {
    "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629",
    "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129",
    "direcao": "entrada",
    "tipo": "texto",
    "texto": "Oi, queria remarcar minha consulta",
    "estado": "pendente",
    "erro": "Connection timed out",
    "autor_tipo": "contato",
    "responde_a_id": "ad52f13b-411d-4526-9d07-91f222c74891",
    "id_whatsapp": "wamid.HBgNNTUzMTk4ODg4Nzc3NxUCABEYEjQ",
    "enviada_em": "2026-10-04T13:05:12.481Z",
    "criado_em": "2026-10-04T13:05:12.481Z",
    "editada_em": "2026-10-04T13:05:12.481Z",
    "apagada_em": "2026-10-04T13:05:12.481Z",
    "pela_api": {"chave_id":"71c5c606-a9d7-4270-8891-45d783aa9b71"},
    "modelo": "texto",
    "midia": [
      {
        "id": "94ac725d-5a70-40b2-8fa6-cdd23a32ac85",
        "mime": "image/jpeg",
        "nome_arquivo": "foto.jpg",
        "tamanho": 48213,
        "duracao_s": 1.5
      }
    ]
  }
}
```

**Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela).

## GET /mensagens/{id}/midia — Baixar mídia de uma mensagem

- URL: `GET https://chat.forsi.com.br/api/v1/mensagens/{id}/midia`
- Acesso: escopo `mensagens:ler` · módulo do plano: API
- Sucesso: `200` arquivo binário (`Content-Type` e `Content-Disposition`); `206` para Range, `202` enquanto baixa, `409 midia_indisponivel` se ainda não está disponível e `416` para Range inválido.
- Página: https://docs.forsi.com.br/api/mensagens/baixar-midia (Markdown: https://docs.forsi.com.br/api/mensagens/baixar-midia.md)

Arquivo binário, com Content-Type original e Content-Disposition. Aceita Range, sem redirecionar. Use item com o id de midia[] ou o índice a partir de zero. Mídia sob demanda retorna 202 midia_baixando com Retry-After; mídia indisponível retorna 409 midia_indisponivel.

**Parâmetros no caminho**

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `id` | uuid | sim | No caminho da URL. |

**Parâmetros de consulta (query string)**

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `item` | uuid ou string | não | Id de midia[] ou índice a partir de zero. Sem item, baixa a primeira mídia. |

**Exemplo (curl)**

```bash
curl "https://chat.forsi.com.br/api/v1/mensagens/4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629/midia?item=0" \
  -H "Authorization: Bearer $FORSI_CHAVE" \
  --output arquivo-midia
```

**Resposta `200`**: bytes do arquivo. Confira `Content-Type`, `Content-Disposition` e `Accept-Ranges`; `206` inclui `Content-Range`. Se vier `202`, leia o JSON `erro` e tente novamente após `Retry-After`.

**Erros**: `401`, `403`, `404`, `409`, `416`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela).

## Objetos das respostas

Campos de cada objeto devolvido em `dados`. Campos novos podem aparecer: ignore os que não conhecer.

### EnvioAceito

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `mensagem_id` | uuid | não |  |
| `conversa_id` | uuid | não |  |
| `contato_id` | uuid ou null | não |  |
| `caixa_id` | uuid | não |  |
| `conversa_nova` | boolean | não |  |
| `estado` | "pendente" | não |  |

### Mensagem

| Campo | Tipo | Obrigatório | Notas |
| --- | --- | --- | --- |
| `id` | uuid | não |  |
| `conversa_id` | uuid | não |  |
| `direcao` | "entrada" \| "saida" | não |  |
| `tipo` | string | não | ex.: texto |
| `texto` | string | não |  |
| `estado` | "pendente" \| "enviando" \| "enviada" \| "entregue" \| "lida" \| "tocada" \| "falhou" \| "recebida" \| "nao_confirmado" | não | nao_confirmado: a conexão caiu durante o envio e o WhatsApp não confirmou; a mensagem pode ter chegado. Não é reenviada sozinha. |
| `erro` | string | não |  |
| `autor_tipo` | "contato" \| "usuario" \| "agente" \| "sistema" \| "celular" | não |  |
| `responde_a_id` | uuid | não |  |
| `id_whatsapp` | string | não |  |
| `enviada_em` | data e hora ISO 8601 | não |  |
| `criado_em` | data e hora ISO 8601 | não |  |
| `editada_em` | data e hora ISO 8601 | não |  |
| `apagada_em` | data e hora ISO 8601 | não |  |
| `pela_api` | object | não |  |
| `pela_api.chave_id` | uuid | não |  |
| `modelo` | string | não | Nome do modelo, quando tipo = modelo. |
| `midia` | lista de object | não |  |
| `midia[].id` | uuid | não |  |
| `midia[].mime` | string | não |  |
| `midia[].nome_arquivo` | string | não |  |
| `midia[].tamanho` | integer | não |  |
| `midia[].duracao_s` | number | não |  |

---

Página: https://docs.forsi.com.br/api/mensagens · Índice: https://docs.forsi.com.br/llms.txt · Tudo num arquivo: https://docs.forsi.com.br/llms-full.txt
