# Fiwano — Documentação da API

REST API unificada para WhatsApp, Instagram e Facebook Messenger.

| | |
|---|---|
| **Base URL** | `https://fiwano.com/api/v1` |
| **Formato** | JSON |
| **Auth** | Header `X-API-Key` |

---

## Conteúdo

A documentação completa da API Fiwano em um único arquivo. As seções abaixo aparecem nesta ordem.

1. **Autenticação** — Chaves de API, o header X-API-Key e Authorization: Bearer.
2. **Compatibilidade** — Política de compatibilidade da Fiwano: o contrato da API v1 é estável, toda mudança é aditiva, e o que uma integração precisa fazer para continuar compatível.
3. **Início Rápido** — Crie sua primeira integração Fiwano em cinco minutos: gere uma chave de API, conecte um canal Meta, envie uma mensagem e receba webhooks assinados.
4. **Canais** — Conecte, consulte, atualize, relicencie e reconecte canais do WhatsApp, Instagram e Facebook Messenger pela API Fiwano e pelo fluxo de setup hospedado.
5. **Enviando Mensagens** — Envie texto, mídia e templates do WhatsApp por uma API, com comportamento de entrega por canal, tentativas automáticas e webhooks de status.
6. **Recebendo Mensagens** — Receba eventos do WhatsApp, Instagram e Messenger por webhooks assinados: mensagens recebidas, ecos de mensagens enviadas fora da Fiwano, mídia e status de entrega.
7. **Modelos do WhatsApp** — Crie e gerencie templates do WhatsApp, entenda os estados de aprovação da Meta, configure variáveis e envie templates fora da janela de 24 horas.
8. **Capacidades e Limites** — Compare recursos do WhatsApp, Instagram e Messenger, planos de licença, limites de taxa e mídia e janelas de mensagens específicas por canal.
9. **Erros** — Erros HTTP da API Fiwano e os códigos de erro da Meta de um envio que falhou no WhatsApp, Instagram ou Messenger, com o significado e o que fazer.
10. **Assinaturas e Cobrança** — Consulte a assinatura de cada canal, entenda ciclos de teste e cobrança e trate períodos de carência, expiração e reatribuição de canais.
11. **Integração n8n** — Use o node comunitário verificado da Fiwano no n8n para receber e enviar mensagens de WhatsApp, Instagram DM e Messenger em automações e fluxos de IA.
12. **Referência da API (OpenAPI)** — O contrato completo e legível por máquina da API pública /api/v1, gerado a partir do serviço em produção.

---

## Autenticação

Todas as requisições à API exigem uma chave de API no header `X-API-Key`. Clientes que só aceitam bearer token podem enviar a mesma chave como `Authorization: Bearer YOUR_API_KEY`; quando os dois headers estão presentes, vale o `X-API-Key`.

Crie uma chave na página **API Keys** do portal. A chave completa é exibida **apenas uma vez** — guarde-a com segurança. Chaves perdidas não podem ser recuperadas; revogue e crie uma nova.

```bash
curl https://fiwano.com/api/v1/channels \
  -H "X-API-Key: YOUR_API_KEY"
```

Todas as chaves começam com `mip_live_`. As chaves são armazenadas como hash do nosso lado.

Chaves de API são secretas: chame a API a partir do seu servidor ou de funções de backend, nunca de código no lado do cliente. A API não aceita requisições cross-origin do navegador (CORS).

---

## Compatibilidade

A Fiwano não tem números de versão. O contrato da API é o `v1` (`https://fiwano.com/api/v1`), estável desde o lançamento público em março de 2026. O serviço é atualizado continuamente; os novos recursos aparecem no [changelog](/br/changelog) (com feed Atom).

**Toda mudança no `v1` é aditiva:**

- novos endpoints;
- novos parâmetros e campos **opcionais** de requisição;
- novos campos em respostas e payloads de webhook;
- novos tipos de evento de webhook e novos valores em conjuntos abertos, como status de entrega, dicas de erro e o tipo de mensagem recebida `data.type` (um tipo desconhecido é tratado como `unsupported`).

**O que não muda:** endpoints existentes, nomes, tipos e significados dos campos; a autenticação por `X-API-Key` (`Authorization: Bearer` é uma alternativa aceita); o esquema de assinatura dos webhooks. Novos tipos de evento de webhook **nunca são ativados nos seus canais sem uma ação sua** — você faz opt-in por canal via `webhook_events`.

**O que sua integração precisa fazer** para continuar compatível: ignorar campos que não conhece e ignorar tipos de evento que não ativou. Não trate um campo desconhecido ou um novo valor em um conjunto aberto como erro.

Se uma mudança incompatível um dia for inevitável, ela sairá como uma nova versão da API ao lado do `v1`, anunciada no changelog e por e-mail com antecedência. O `v1` continua funcionando.

---

## Início Rápido

A Fiwano coloca WhatsApp, Instagram e Messenger atrás de uma única REST API. Este
é o ciclo central em **quatro passos** — autenticar, conectar um canal, receber uma
mensagem, responder — mais um quinto opcional para enviar mensagens fora da janela
de 24 horas. Toda requisição usa a base URL `https://fiwano.com` e leva sua chave no
header `X-API-Key`.

### 1. Obtenha e verifique sua chave de API

Toda conta nova ganha um **período de teste gratuito de 7 dias com funcionalidade
completa** — todos os tipos de canal, mídia e modelos, sem necessidade de cartão.
Abra **API Keys** no [portal](https://fiwano.com) e crie uma chave: a chave completa
é exibida **apenas uma vez**, começa com `mip_live_` e é armazenada somente como
hash — chaves perdidas não podem ser recuperadas, então revogue e recrie se preciso.
Guarde-a em uma variável de ambiente (ex.: `FIWANO_API_KEY`); nunca a deixe fixa no
código nem a comite.

**Verifique se a chave funciona** listando os canais:

```bash
curl https://fiwano.com/api/v1/channels -H "X-API-Key: $FIWANO_API_KEY"
```

Uma chave válida em uma conta nova (ainda sem canais) retorna **`200`** com uma
lista vazia — esse é o sinal de sucesso de que você está autenticado e pronto para
o passo 2:

```json
{ "channels": [], "total": 0 }
```

Uma chave inválida ou ausente retorna `401`. Formatos de erro e códigos de status:
[Erros](/br/documentation/errors).

### 2. Conecte um canal

Há **duas formas de conectar** — escolha a que combina com quem é o dono da conta,
ambas detalhadas em [Canais](/br/documentation/channels):

- **Seu próprio canal** — conecte-o no [portal](https://fiwano.com)
  (Channels → Connect), sem código. Melhor quando você mesmo opera as contas. Os
  pré-requisitos (o ativo deve pertencer a um Meta Business Portfolio, e você deve
  ser admin dele) estão detalhados lá.
- **Os canais dos seus usuários finais** — um fluxo OAuth incorporado que seu app
  conduz: cadastre um `redirect_uri` na whitelist, crie uma setup URL, o usuário
  conclui o login da Meta dentro dela, e você troca o `code` retornado (de uso
  único) por um `channel_id`.

Operações OpenAPI deste passo:

- Gerenciar canais: `GET /api/v1/channels`, `GET /api/v1/channels/{channel_id}`, `PATCH /api/v1/channels/{channel_id}`, `DELETE /api/v1/channels/{channel_id}`
- Fluxo de conexão incorporado: `POST /api/v1/channels/setup-url`, `POST /api/v1/channels/exchange-code`
- Whitelist de redirect URI: `GET /api/v1/redirects`, `POST /api/v1/redirects`, `DELETE /api/v1/redirects/{redirect_id}`

**Sucesso:** você tem um `channel_id` (o fluxo incorporado o retorna direto do
`exchange-code`), e `GET /api/v1/channels` agora lista o canal com
`"is_active": true` e `"health": {"status": "ok", …}` — ele pode enviar e
receber. Esse `channel_id` é o que você
passa em toda chamada de envio e recebimento daqui em diante.

### 3. Receba uma mensagem

Responder mensagens recebidas é o caso de uso central da Fiwano, então configure o
recebimento **antes** do envio. Duas partes:

**1. Ative os eventos no canal.** A entrega é opt-in — **por padrão nenhum evento é
entregue**. Defina `webhook_events` (e uma `webhook_url`) no canal e ative apenas os
eventos que você realmente trata (comece por `message.received`). A lista de eventos
por canal e como configurá-la estão em [Canais](/br/documentation/channels).

**2. Trate o webhook.** A Fiwano envia um POST de cada evento ativado para a sua
`webhook_url`. Seu endpoint **deve verificar a `X-Webhook-Signature`** (HMAC-SHA256
com o `webhook_secret` do canal) e **responder HTTP 2xx em ~5 segundos** — caso
contrário a Fiwano tenta novamente com backoff e envia um e-mail. Confirme primeiro
e só depois faça o trabalho lento, como gerar uma resposta de IA. Os formatos de
payload e a verificação de assinatura estão em
[Recebendo Mensagens](/br/documentation/webhooks).

Um `message.received` recebido carrega os dois identificadores que você precisa para
responder (destacados abaixo):

```jsonc
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",   // ← qual dos seus canais o recebeu
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",              // ← quem enviou — responda para este
    "from_name": "John Doe",
    "type": "text",
    "text": "Hello!"
  }
}
```

- **`channel_id`** (nível superior) — o canal em que a mensagem chegou.
- **`data.from`** — o id do remetente: número de telefone (WhatsApp), IGSID (Instagram)
  ou PSID (Facebook). É exatamente o que você passa de volta como `recipient`.

A partir de um handler, você também costuma chamar:

- `PATCH /api/v1/channels/{channel_id}` — define ou atualiza `webhook_events` / `webhook_url`
- `GET /api/v1/media/{media_id}` — baixa a mídia recebida
- `GET /api/v1/channels/{channel_id}/profile/{user_id}` — consulta o perfil do remetente

**Sucesso:** mande uma mensagem para o seu canal conectado de um aparelho real; seu
endpoint recebe um webhook `message.received` com assinatura válida e retorna 2xx.
Você já está recebendo.

### 4. Responda a ela

Com o recebimento no lugar, faça o envio. O caso do dia a dia é uma **resposta em
formato livre dentro da janela de 24 horas** depois que um usuário te escreve —
texto ou mídia, sem aprovação. Este é o movimento central: responda ao remetente
devolvendo os **mesmos** identificadores — o `channel_id` do webhook como
`channel_id`, e `data.from` como `recipient`:

```bash
curl -X POST https://fiwano.com/api/v1/messages/send \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "text": "Thanks for your message!"}'
```

- Enviar: `POST /api/v1/messages/send` (texto), `POST /api/v1/messages/send-media` (mídia)

**Sucesso:** a chamada retorna `success: true` com um `message_id` — confira: um
`200` ainda pode dizer `"status": "failed"` (veja
[Entrega e novas tentativas](/br/documentation/sending-messages#delivery-and-retries)). Se você
ativou os eventos de entrega no passo 3, você então recebe os webhooks
`message.sent` / `message.delivered` acompanhando-a. Leia
[Enviando Mensagens](/br/documentation/sending-messages) para mídia e mais.

Isso cobre o ciclo central — conectar, receber, responder. O passo 5 é opcional.

### 5. Modelos do WhatsApp — mensagens fora da janela de 24 horas (opcional)

Mensagens em formato livre só alcançam um usuário **dentro** da janela de 24 horas.
Para iniciar uma conversa, ou para responder depois que a janela fechou, o WhatsApp
exige um **modelo** (template) pré-aprovado (somente WhatsApp). Pule este passo se
você sempre responde dentro da janela — veja a janela de 24 horas em
[Capacidades](/br/documentation/capabilities#messaging-windows-24h).

Leia [Modelos do WhatsApp](/br/documentation/templates) para o ciclo de criação/revisão
e então envie o modelo aprovado.

- Enviar um modelo: `POST /api/v1/messages/send-template`
- Gerenciar modelos: `GET /api/v1/channels/{channel_id}/templates`, `POST /api/v1/channels/{channel_id}/templates`, `GET|PUT|DELETE /api/v1/channels/{channel_id}/templates/{template_id}`

### Próximos passos

- **Sem código?** Use o [node n8n](/br/documentation/n8n) verificado — mesmos canais,
  mesmos eventos, arrastar e soltar.
- **Mídia e modelos** — [Enviando Mensagens](/br/documentation/sending-messages).
- **Limites, janelas e planos** — [Capacidades](/br/documentation/capabilities).
- **O contrato completo legível por máquina** — [Referência da API](/documentation/api).
</content>

---

## Canais

Um **canal** é um ativo Meta conectado — um número de WhatsApp, uma conta do Instagram
ou uma Página do Facebook — pelo qual você envia e recebe mensagens. Esta página
cobre como conectar, gerenciar e reconectar canais. Os schemas de campos estão na
**[Referência da API](/documentation/api)**.

### Pré-requisitos

Antes de conectar qualquer canal — WhatsApp, Instagram ou Facebook Messenger — garanta
que as condições abaixo sejam atendidas. Elas valem igualmente para o fluxo do
Portal e o fluxo da API; se uma das duas primeiras faltar, a Meta interrompe o popup de
OAuth antes que um canal possa ser criado.

- **O ativo pertence a um Meta Business Portfolio** (Business Manager). O
  "ativo" é a WABA do número de WhatsApp, a Página do Facebook ou — no caso do Instagram —
  uma conta do Instagram Business ou Creator vinculada a uma Página do Facebook que seja
  de propriedade de um Business Portfolio.
- **O usuário do Facebook que faz login tem direitos completos de admin** nesse Business
  Portfolio e no próprio ativo. Um usuário sem papel de admin vê a opção relevante
  desabilitada no popup.
- **O Fiwano é o app no controle das conversas** (Instagram e Facebook
  Messenger). A Meta dá o controle de cada conversa a um único app por vez
  (*Conversation Routing*), então o Fiwano precisa ser o *Default routing app*
  para responder. Defina em Página do Facebook → Settings → Page setup →
  *Instagram conversation routing* / *Messenger conversation routing* (para
  Instagram sem Página: Meta Business Suite → Settings → Integrations →
  *Conversation Routing*), desative *Take control of conversations* nos outros
  apps ou desconecte-os, e não atenda esses chats pelo Meta Business Suite /
  caixa de entrada da Página nem com a Meta AI — qualquer um dos dois passa o
  controle para o inbox da própria Meta. Se o Fiwano não estiver no controle, as
  mensagens recebidas ainda chegam ao seu webhook, mas as respostas são
  rejeitadas com o [erro `10`](/br/documentation/errors#error-10).

### Opção A: Via Portal (self-service)

Use isto para conectar **seus próprios** canais, sem necessidade de código.

1. Vá em **Channels → Connect Channel** no portal.
2. Selecione o tipo de canal (WhatsApp, Instagram ou Facebook Messenger).
3. Conclua o fluxo de OAuth da Meta na janela popup.
4. Configure a **Webhook URL** e selecione os **Webhook Events** nas configurações do canal.
5. Por padrão, nenhum evento está habilitado — selecione quais eventos encaminhar ao seu endpoint.
6. Gere um **segredo do webhook** para que as entregas sejam assinadas — veja [Segredo do webhook](#webhook-secret).

A webhook URL deve ser uma URL HTTPS absoluta e acessível pela Fiwano. Portas
explícitas de 1 a 65535 são suportadas; credenciais embutidas e fragmentos de URL não são.

### Opção B: Via API (programática)

Use isto quando sua aplicação conecta canais **em nome dos seus usuários finais**.

**Passo 1 — Cadastre seu redirect URI na whitelist.** Por segurança, o usuário só pode ser
redirecionado de volta a uma URL que você tenha pré-registrado para a sua chave de API.
Registre a(s) URL(s) onde os usuários chegam após o OAuth (coringas são permitidos, ex.:
`https://*.example.com/callback`):

```bash
curl -X POST https://fiwano.com/api/v1/redirects \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uri_pattern": "https://yourapp.com/callback"}'
```

Os padrões de redirect URI devem usar HTTPS e não podem apontar para localhost nem
para um endereço de loopback. Portas explícitas de 1 a 65535 são suportadas,
inclusive portas HTTPS não padrão, como `https://yourapp.com:4426/callback`. URIs
exatos são mais seguros e recomendados. Quando um curinga for necessário, ele pode
aparecer no caminho/query ou como um rótulo completo mais à esquerda do hostname
(`*.example.com`), mas não pode substituir todo o hostname, parte de um rótulo ou
a porta. Credenciais embutidas, fragmentos de URL e as chaves de query reservadas
`code`, `status`, `channel_type` e `error` são rejeitados.

Para associar um fluxo de setup ao tenant ou administrador autenticado, gere um
nonce opaco, de alta entropia e uso único, armazene-o no servidor junto com esse
contexto e coloque somente o nonce no redirect URI. Cadastre um padrão restrito,
como `https://yourapp.com/callback?state=*`, e solicite a setup URL com
`https://yourapp.com/callback?state=NONCE_BASE64URL`. A Fiwano preserva `state` e
acrescenta seus próprios parâmetros, por exemplo
`?state=NONCE_BASE64URL&code=...&status=success&channel_type=whatsapp`. Use um
valor URL-safe e não coloque IDs de tenant/usuário nem outros dados sensíveis
diretamente na URI.

Você os gerencia com `GET /api/v1/redirects` e `DELETE /api/v1/redirects/{id}`.

**Passo 2 — Solicite uma setup URL.** Passe um dos seus redirect URIs da whitelist. A
URL é válida até o `expires_at` retornado na resposta — abra-a em um navegador ou popup
para o usuário:

```bash
curl -X POST https://fiwano.com/api/v1/channels/setup-url \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel_type": "whatsapp", "redirect_uri": "https://yourapp.com/callback"}'
```

O mesmo endpoint também reconecta canais; não existe uma API de reconnect
separada. Se o usuário conectar uma conta Meta que já é um canal seu,
`exchange-code` retorna esse `channel_id` existente — veja
[Reconectando um canal](#reconnecting-an-inactive-channel).

A requisição é recusada com `402` quando a conta não tem assinatura ativa, e com
`409` quando todos os slots de assinatura daquele tipo de canal já estão ocupados
e nenhum dos canais que os ocupam pode ser reconectado por este fluxo. O corpo do
`409` é estruturado: `detail.code` é `no_free_slot` e `detail.occupied_by` lista
os canais que ocupam os slots — veja [slots de assinatura](#subscription-slots)
para saber como liberar um.

**Passo 3 — O usuário conclui o OAuth da Meta.** Após a aprovação, o usuário é
redirecionado ao seu `redirect_uri` com um parâmetro `code` de uso único:

```
https://yourapp.com/callback?code=abc123...
```

Em caso de falha, o redirect leva dois query params — ramifique sua lógica apenas no
`error`:

| Query param | Como usar |
|---|---|
| `error` | Código legível por máquina. **Ramifique nisto.** `access_denied` — o usuário cancelou ou não concluiu o diálogo da Meta. `slot_occupied` — o usuário conectou uma conta Meta *diferente* da que ocupa o slot da sua assinatura; `message` indica o canal a reconectar ou liberar (veja [slots de assinatura](#subscription-slots)). `session_expired` — a setup URL expirou antes de o fluxo terminar; solicite uma nova. `setup_failed` — qualquer outra coisa que interrompeu o setup (ex.: nenhuma conta do Instagram Business estava acessível com as permissões concedidas). |
| `message` | Explicação legível por humanos em inglês, codificada em URL, segura para exibir ao usuário. **Formato livre e pode mudar — nunca faça parse ou ramifique no texto dela.** |

Exemplo de redirect de falha:

```
https://yourapp.com/callback?error=setup_failed&message=We%20couldn%27t%20access%20any%20Instagram%20Business%20account...
```

**Passo 4 — Troque o code.** Em até 5 minutos (uso único), troque o code
pelo canal. Você pode configurar o webhook na mesma chamada:

```bash
curl -X POST https://fiwano.com/api/v1/channels/exchange-code \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "abc123...",
    "webhook_url": "https://yourapp.com/webhooks/meta",
    "webhook_events": ["message.received", "message.delivered", "message.failed"]
  }'
```

A resposta retorna seu `channel_id` (guarde-o — toda outra chamada o utiliza). Quando
você define `webhook_url` e não fornece um `webhook_secret`, a Fiwano **gera um
automaticamente** e o retorna aqui. Ele é retornado **apenas nesta resposta** — o `GET`
nunca o mostra de novo — então guarde-o para verificar as assinaturas dos webhooks
([Segredo do webhook](#webhook-secret)). Todos os campos exceto `code` são opcionais e
podem ser definidos depois via `PATCH /api/v1/channels/{id}`.

### Eventos de webhook

A entrega de webhooks é **opt-in por canal**: por padrão **nenhum evento é entregue**.
Você escolhe o que recebe definindo `webhook_events` — na chamada de conexão, no Portal,
ou depois via `PATCH /api/v1/channels/{id}`. Até fazer isso, seu endpoint não recebe nada.

A maioria dos eventos existe em todos os canais; `message.sent` é só do WhatsApp e
`conversation.referral` só do Instagram e do Facebook. A lista completa com os
payloads está na página **[Recebendo Mensagens](/br/documentation/webhooks#event-types)**.
Um evento que você listar e que não seja válido para o tipo de canal é ignorado, não é
um erro.

Um evento tem um ajuste extra: `message.echo` (cópias de mensagens que sua empresa envia
fora da Fiwano) entrega apenas a mensagem por padrão. Defina o campo booleano
`echo_statuses` do canal — na chamada de conexão, via `PATCH /api/v1/channels/{id}` ou no
Portal — para receber também os status delivered/read das mensagens ecoadas pelos seus
eventos de status habituais. Detalhes: **[message.echo](/br/documentation/webhooks#message-echo)**.

**Seu endpoint cumpre a outra metade desse contrato.** Uma vez habilitados os eventos, a
Fiwano faz POST de cada um para seu `webhook_url`, e seu endpoint **precisa responder com
HTTP 2xx em até 5 segundos**; caso contrário, a Fiwano tenta novamente e envia e-mail para
você — veja **[Política de novas tentativas](/br/documentation/webhooks#retry-policy)**. Por
isso, habilite **apenas os eventos que você realmente trata** e retorne 2xx assim que
aceitar o payload (faça o trabalho mais lento depois).

### Segredo do webhook

O `webhook_secret` é a chave HMAC que a Fiwano usa para **assinar as entregas de
webhook**, para que seu endpoint possa confirmar que a requisição realmente veio da
Fiwano e não foi alterada no caminho. Quando um canal tem um segredo, cada entrega traz
um cabeçalho `X-Webhook-Signature: sha256=<hmac>` — veja
**[Verificando assinaturas](/br/documentation/webhooks#verifying-signatures)** para o trecho de verificação. Um canal sem
segredo recebe entregas **sem assinatura**.

Como um segredo aparece pela primeira vez difere conforme você conecta — e este é o
único ponto em que o Portal e a API se comportam propositalmente de forma diferente:

- **Portal (Opção A):** um canal novo **não tem segredo**, e salvar uma webhook URL não
  cria um. Defina-o você mesmo nas configurações do canal: clique em **Generate random**
  para um segredo aleatório de 64 caracteres, ou digite o seu próprio e clique em
  **Save** (16–64 caracteres). O valor é revelado **uma única vez**, logo em
  seguida.
- **API (Opção B):** quando você define `webhook_url` e o canal ainda não tem segredo, a
  Fiwano **gera um automaticamente** (hex de 64 caracteres) e o retorna na resposta de
  `exchange-code` / `PATCH /api/v1/channels/{id}` — ou seja, canais conectados via API
  são **assinados por padrão**. Para usar um valor específico, forneça o seu próprio
  `webhook_secret` (**no máximo 64 caracteres**) nessa mesma chamada.

**Lendo de volta.** O valor só é retornado no momento em que é definido ou alterado —
na revelação única do Portal, ou nas respostas de `exchange-code` e
`PATCH /api/v1/channels/{id}`. `GET /api/v1/channels` e `GET /api/v1/channels/{id}`
nunca o retornam; eles apenas informam `has_webhook_secret: true | false`. **Guarde o
valor quando ele for exibido** — se você o perder, a única opção é definir um novo.

**Rotacionando.** Defina um novo segredo a qualquer momento fornecendo um novo
`webhook_secret` ao `PATCH /api/v1/channels/{id}`, ou com as ações **Generate random** /
**Save** do Portal. Atualizar apenas `webhook_url`/`webhook_events` mantém o segredo
intacto. A mudança tem efeito na **próxima entrega** — não há janela de sobreposição,
então troque seu verificador para o novo segredo no mesmo momento, ou as assinaturas não
vão corresponder.

**Restrições e recomendações.**

- Use uma string aleatória de alta entropia com **16–64 caracteres** (o Portal exige
  o mínimo de 16 caracteres; o campo armazena até 64). Segredos gerados
  automaticamente têm 64 caracteres hex — prefira-os, a menos que tenha um motivo para
  usar o seu próprio.
- O segredo é **por canal** — cada canal tem o seu, independente dos demais.
- Reconectar um canal **mantém** o segredo existente (veja
  [Reconectando um canal](#reconnecting-an-inactive-channel) abaixo).
- Trate-o como uma senha: armazene-o em um gerenciador de segredos, nunca o inclua no
  controle de versão e verifique as assinaturas usando comparação de tempo constante
  (como no trecho de verificação).

### Gerenciando canais

| Tarefa | Endpoint |
|---|---|
| Listar todos os canais (ativos e inativos), cada um com seu estado de assinatura atual | `GET /api/v1/channels` |
| Inspecionar um canal | `GET /api/v1/channels/{id}` |
| Atualizar webhook URL / secret / events, ou o vínculo de assinatura | `PATCH /api/v1/channels/{id}` |
| Desativar um canal | `DELETE /api/v1/channels/{id}` |

Cada canal traz um bloco `subscription` descrevendo seu estado de cobrança — veja
**[Assinaturas e Cobrança](/br/documentation/subscriptions)** para o que as
combinações significam. As listas completas de campos estão na **[Referência da API](/documentation/api)**.

**A desativação é um soft delete.** O `DELETE` impede o canal de enviar e
receber, mas não o apaga — seu `channel_id` e histórico são preservados para que
você possa reconectar depois. O canal também continua pertencendo à mesma conta
Fiwano: a desativação não libera seu número do WhatsApp, conta do Instagram ou
Página do Facebook para conexão com outra conta Fiwano. Se o canal precisar ser
movido entre contas, entre em contato com `support@fiwano.com`.

### Slots de assinatura

Cada assinatura concede **um slot por tipo de canal** — um WhatsApp, um
Instagram, um Facebook. Um slot permanece ocupado enquanto um canal estiver
vinculado a ele, **inclusive um canal desativado**: é esse vínculo que permite
reconectar aquele canal depois sem comprar outra assinatura.

`GET /api/v1/subscriptions` mostra qual canal ocupa cada slot e quantos estão
livres; cada canal, por sua vez, informa seu próprio `subscription.id`.

Envie `subscription_id` em `PATCH /api/v1/channels/{channel_id}` para mudar isso.
Um ID de assinatura move o canal para lá — sem indisponibilidade, e ele não
precisa ser desativado antes, mas mover para uma assinatura Starter interrompe o
envio de mídia e de templates imediatamente. Uma string vazia libera o slot, e
isso só é permitido para um canal já desativado com
`DELETE /api/v1/channels/{channel_id}`, de modo que um slot nunca é liberado como
efeito colateral de uma atualização de configuração.

**Liberar um slot é, na prática, permanente.** Assim que outro canal ocupar o slot
liberado, o canal liberado não poderá mais ser reconectado até que haja um slot
livre novamente. Ele não é apagado e sua identidade Meta continua pertencendo à
sua conta Fiwano — mas trate a liberação como aposentar aquele canal, não como
pausá-lo.

Substituindo um canal quando você tem apenas uma assinatura:

```text
GET    /api/v1/subscriptions           → encontre a assinatura e seu slot ocupado
DELETE /api/v1/channels/{old_id}       → desative o canal que será substituído
PATCH  /api/v1/channels/{old_id}       → {"subscription_id": ""} libera o slot
POST   /api/v1/channels/setup-url      → usuário conecta a nova conta Meta
POST   /api/v1/channels/exchange-code  → novo canal ocupa o slot livre
```

### Saúde do canal

Um canal pode continuar ativo e mesmo assim não conseguir funcionar, quando a Meta
deixa de permitir que a Fiwano trabalhe com a conta ou o número: o app foi removido
nas configurações do Meta Business, uma permissão obrigatória foi revogada, a Página
ou a conta do WhatsApp ficou indisponível, o número de WhatsApp não está na WhatsApp
Business Platform (a conexão pelo *WhatsApp Business App* não foi concluída) ou a
Meta bloqueou a conta. A Fiwano então marca o canal como **Action required** no
portal e envia um e-mail ao dono da conta; o aviso e o e-mail dizem o que fazer.
Até a causa ser resolvida, os envios podem falhar — por exemplo com o [erro `190`](/br/documentation/errors#send-error-codes).

Todo canal na API traz o mesmo estado em `health`:

```json
"health": {
  "status": "action_required",
  "reason": "(#190) Error validating access token: …",
  "since": "2026-10-06T10:00:00Z"
}
```

`status` é `ok` ou `action_required` — decida com base nele. `reason` é um texto
legível, normalmente o próprio erro da Meta com o código, que você pode pesquisar
na documentação da Meta; não decida com base nele. `since` é quando o problema foi
detectado. `health` volta a `ok` sozinho quando a Meta volta a permitir o acesso, e
na reconexão. `is_active` continua `true`: só vira `false` quando o canal é
desativado. Falhas de mensagens individuais não fazem parte de `health` — elas
chegam na resposta do envio e no webhook `message.failed`, e aparecem na página
**Delivery problems** do canal no portal.

### Reconectando um canal

Reconecte um canal desativado, ou marcado como *Action required*, executando o
**mesmo fluxo de conexão novamente para a mesma conta Meta** (mesmo número de
WhatsApp, conta do Instagram ou Página do Facebook). Quando a Meta bloqueou a conta,
resolva primeiro na Meta — só reconectar não remove o bloqueio:

- O canal existente é **atualizado no lugar** — seu `channel_id`, webhook
  URL/secret/events e histórico são preservados. Nenhum canal novo é criado e o seu
  mapeamento de `channel_id` armazenado continua válido.
- A reconexão exige uma **assinatura ativa**: o canal ainda deve ter uma, ou
  você deve ter um slot livre. Caso contrário, o `setup-url` é recusado (`402` ou
  `409`, veja acima) — vincule uma assinatura primeiro, na página Billing do portal
  ou via `PATCH /api/v1/channels/{channel_id}`.
- Se o usuário concluir o diálogo para uma conta Meta **diferente** enquanto o
  canal desativado ainda ocupa o slot, o fluxo é recusado no final com
  `error=slot_occupied` e nada é criado. Reconecte a mesma conta ou libere o slot
  antes.
- Uma conta Meta pertencente a outra conta Fiwano não pode ser conectada, mesmo
  quando o canal estiver inativo. Se o canal for seu, entre em contato com
  `support@fiwano.com` para solicitar a liberação da titularidade.

---

## Enviando Mensagens

O Fiwano tem três endpoints de envio — texto simples, mídia e modelos do WhatsApp. Todos
recebem um `channel_id` e um `recipient`. O formato do `recipient` depende do
canal (número de telefone para WhatsApp, IGSID para Instagram, PSID para Facebook) — veja
a linha de destinatário em [Capacidades](/br/documentation/capabilities#channel-capabilities). Os
schemas completos de request/response estão na [Referência da API](/documentation/api); esta
página é o guia por tarefa.

### Enviar mensagens de WhatsApp com a API

Para uma resposta normal de WhatsApp dentro da janela de atendimento de 24 horas,
use o endpoint de texto simples abaixo com um `channel_id` de WhatsApp e o número
de telefone do destinatário. Você autentica com sua `X-API-Key` da Fiwano; não
precisa de uma chave separada da API da Meta ou do WhatsApp na sua aplicação.

Fora da janela de 24 horas, o WhatsApp exige uma mensagem de template aprovada. Isso
usa `/api/v1/messages/send-template` e está descrito em
[Mensagens de modelo](#template-messages). Instagram DM e Facebook Messenger usam o
mesmo endpoint de texto para respostas comuns, com IGSID ou PSID como `recipient`.

### Mensagens de texto

`POST /api/v1/messages/send` — funciona em todos os tipos de canal.

```bash
curl -X POST https://fiwano.com/api/v1/messages/send \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "text": "Hello! Your order is ready."}'
```

A resposta carrega um `message_id` (um UUID do Fiwano) que todo webhook posterior de status
de entrega referencia. O texto tem um limite de tamanho por plataforma (WhatsApp 4096,
Facebook 2000, Instagram 1000) — texto acima do limite é rejeitado com `400 text_too_long`
antes de chamar a Meta. O Fiwano **não** divide automaticamente; divida do seu lado para
preservar seu próprio fatiamento e ordenação. Um `text` vazio ou só com espaços é
rejeitado com `422`.

Um `recipient` obviamente inválido (vazio, sem dígitos ou um PSID/IGSID não numérico) é
rejeitado com `400 invalid_recipient` antes de chamar a Meta; qualquer outro número de
WhatsApp é passado à Meta como está, e a rejeição da própria Meta volta como
`status: "failed"`. Veja [Erros HTTP](/br/documentation/errors#http-errors).

### Mensagens de mídia

`POST /api/v1/messages/send-media` — **licença Pro obrigatória.** A Meta busca o arquivo
diretamente de `media_url`. Passe `media_type`
(`image`, `audio`, `video`, `document` ou `sticker` — veja [Figurinhas](#stickers))
e uma `media_url` HTTPS.

**Use uma URL assinada para conteúdo não público** — pré-assinada de S3/GCS/R2, SAS do
Azure ou uma URL assinada por HMAC no seu próprio servidor, com expiração ≥ 20 min para
continuar válida durante as novas tentativas em segundo plano. Uma URL
pública é acessível por qualquer um que a descubra.

**Mantenha os arquivos pequenos e a hospedagem rápida.** A Meta baixa o arquivo
enquanto sua requisição espera: uma imagem comprimida em um host rápido é aceita em
poucos segundos, enquanto um arquivo grande em um host lento pode levar um minuto ou
mais. Arquivos menores significam entrega mais rápida e previsível e menos envios
concluídos em segundo plano — veja [Tempo de resposta](#response-time).

```bash
curl -X POST https://fiwano.com/api/v1/messages/send-media \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "channel_id": "a1b2c3d4e5f67890",
    "recipient": "1234567890",
    "media_type": "image",
    "media_url": "https://my-bucket.s3.amazonaws.com/photo.jpg?X-Amz-Signature=...&X-Amz-Expires=1800",
    "caption": "Your order photo"
  }'
```

Sempre verifique `success` e `status`. Falhas permanentes incluem um `error_code` da Meta —
por exemplo `131052` quando a Meta não consegue baixar a URL; as temporárias, como
`131053`, retornam `queued` e são repetidas como o texto. Os limites de tamanho de arquivo estão em
[Capacidades](/br/documentation/capabilities#outbound-media-size) e a tabela
completa de error codes está em [Erros](/br/documentation/errors#send-error-codes).

#### Figurinhas

`media_type: "sticker"` envia uma figurinha (sticker); o que ela exige depende do
canal, porque as plataformas da Meta são diferentes:

| Canal | Campo | O que a Meta aceita |
|---|---|---|
| WhatsApp | `media_url` | um arquivo WebP, 512×512 px, até 100 KB (estática) ou 500 KB (animada) |
| Facebook Messenger | `sticker_id` | uma figurinha do catálogo da própria Meta — `369239263222822` é o joinha — ou o `media.sticker_id` de uma figurinha que um usuário enviou a você |
| Instagram | — | não existe mensagem de figurinha; envie uma `image` |

```bash
curl -X POST https://fiwano.com/api/v1/messages/send-media \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890",
       "media_type": "sticker", "media_url": "https://my-bucket.s3.amazonaws.com/obrigado.webp?X-Amz-Signature=..."}'
```

`caption` e `filename` são ignorados em figurinhas. O campo errado para o canal —
`sticker_id` no WhatsApp, `media_url` no Messenger, qualquer figurinha no
Instagram — responde `400 invalid_media_request` antes de chamar a Meta
(`reason` diz qual). Um WebP que viola as regras do WhatsApp, ou um WebP enviado
como `image`, falha na hora com o código `131053` da Meta e uma dica; não há nova
tentativa. Uma figurinha recebida chega como `image` com `media.sticker: true` —
veja [Recebendo Mensagens](/br/documentation/webhooks#media-messages).

### Tempo de resposta

`POST /messages/send-media` é **síncrono e pode ser lento**. O Fiwano nunca baixa
seu arquivo: entregamos a `media_url` à Meta e **a Meta busca o arquivo dentro da
sua requisição**. A espera é portanto proporcional ao tamanho do arquivo e à
velocidade da sua própria hospedagem. Uma chamada de 12 segundos para um arquivo
grande é normal. O Fiwano aguarda a Meta por até **30 segundos**; se a Meta ainda
não tiver respondido, a chamada retorna `queued` e o Fiwano conclui o envio em
segundo plano — veja [Entrega e novas tentativas](#delivery-and-retries).

Envios de texto e de modelo não são afetados — não carregam arquivo e costumam
completar em bem menos de um segundo.

**Configure o timeout do seu cliente HTTP para pelo menos 35 segundos** em
`send-media`. Alguns ambientes limitam isso por você e não conseguem esperar
tanto — o AWS API Gateway para em 29 segundos, e funções serverless costumam ter
padrão de 10–15 segundos.

> **Se o seu cliente atingir o timeout, a mensagem ainda pode ter sido enviada.**
> A Meta pode aceitá-la depois que você parou de esperar. Reenviar então entrega
> a mensagem duas vezes. Só tente novamente após confirmar que a mensagem não
> está na conversa.

Para manter os envios de mídia rápidos, sirva a `media_url` de um armazenamento
próximo aos seus usuários (S3/GCS/R2 com CDN) e mantenha os arquivos bem abaixo
dos [limites de tamanho](/br/documentation/capabilities#outbound-media-size).

### Mensagens de modelo

`POST /api/v1/messages/send-template` — **somente WhatsApp, Pro obrigatório.** Use um
modelo pré-aprovado para iniciar uma conversa fora da janela de 24 horas (veja
[Capacidades](/br/documentation/capabilities#messaging-windows-24h)). Apenas modelos
`APPROVED` podem ser enviados — para criá-los e gerenciá-los, veja
[Modelos do WhatsApp](/br/documentation/templates).

Forneça os valores das variáveis por componente. Modelos **posicionais** (`{{1}}`,
`{{2}}`) recebem arrays:

```bash
curl -X POST https://fiwano.com/api/v1/messages/send-template \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "channel_id": "a1b2c3d4e5f67890",
    "template_name": "order_confirmation",
    "language": "en_US",
    "recipient": "1234567890",
    "variables": {
      "header": ["Summer Sale"],
      "body": ["Pablo", "ORD-123", "25%"],
      "buttons": [{"index": 0, "value": "promo25"}]
    }
  }'
```

Modelos **nomeados** (`{{customer_name}}`) recebem objetos:

```bash
  -d '{
    "channel_id": "a1b2c3d4e5f67890",
    "template_name": "welcome_message",
    "language": "en_US",
    "recipient": "1234567890",
    "variables": {"body": {"customer_name": "Pablo", "order_number": "ORD-123"}}
  }'
```

Omita `variables` por completo se o modelo não tiver nenhuma.

Envios de modelo retornam o mesmo formato de resposta que envios de texto e mídia.
O `message_id` é um UUID do Fiwano; guarde-o para correlacionar os webhooks de entrega:

```json
{
  "success": true,
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "error": null,
  "error_code": null,
  "status": "sent"
}
```

### Entrega e novas tentativas

Os três endpoints de envio respondem `200` com `success`, `message_id`, `status`,
`error` e `error_code`. Um `200` sozinho não significa que a mensagem saiu —
leia `status`:

| `status` | `success` | Significado | O que fazer |
|---|---|---|---|
| `sent` | `true` | A Meta aceitou a mensagem | Acompanhe pelos [webhooks de status de entrega](/br/documentation/webhooks#delivery-status-tracking) |
| `queued` | `true` | O Fiwano está concluindo o envio em segundo plano; o `message_id` é definitivo | Não reenvie. O resultado chega por webhooks: os status habituais ou [`message.failed`](/br/documentation/webhooks#event-types) |
| `failed` | `false` | Não enviada, e não será repetida | Leia `error` e `error_code` — veja [códigos de erro de envio](/br/documentation/errors#send-error-codes) |

Uma requisição que o Fiwano recusa antes de chamar a Meta — `text_too_long`,
`invalid_recipient`, um canal inativo, um limite de taxa — recebe um
[erro HTTP](/br/documentation/errors#http-errors) em vez de `failed`. Em `send` e
`send-media`, uma mensagem `failed` também é informada ao dono do canal por
e-mail.

No portal, a página **Delivery problems** de cada canal (aba Messages) lista as
mensagens dos últimos 7 dias que estão `queued` aguardando nova tentativa (com o
número da tentativa e o próximo horário) ou `failed`, com o código e o texto da
Meta — o mesmo `error` que a API e o `message.failed` retornam. Inclui as
mensagens do WhatsApp que a Meta aceitou e depois informou como falhas.

Um envio fica `queued` por um de dois motivos:

- **A Meta falhou temporariamente** (texto e mídia) — erro de rede, `5xx` da
  Meta, rate limit ou um código de erro temporário. O Fiwano tenta de novo até 7
  vezes ao longo de ~20 minutos. O dono do canal recebe um e-mail após 3
  tentativas falhas e outro se todas falharem; você então recebe
  `message.failed`. `send-template` não repete: qualquer erro que a Meta retorne
  é `failed`, e você decide se e quando reenviar.
- **A Meta respondeu devagar** (qualquer envio) — veja abaixo.

#### Respostas lentas da Meta

Ocasionalmente a Meta leva mais de 30 segundos — às vezes mais de um minuto —
para responder a um envio, na maioria das vezes enquanto baixa um arquivo
grande. A chamada então retorna `queued` e o Fiwano espera a Meta confirmar a
mensagem:

- Normalmente a confirmação chega e os webhooks habituais `message.sent` /
  `message.delivered` / `message.read` vêm em seguida.
- Se a Meta não confirmar nada em alguns minutos, o Fiwano envia uma mensagem de
  texto ou mídia mais uma vez (nunca um modelo). A primeira tentativa pode ter
  sido concluída afinal, então em casos raros o destinatário recebe a mensagem
  duas vezes — uma duplicata é preferível a uma mensagem perdida.
- Se nada for confirmado no fim, a mensagem passa a `failed`: você recebe
  `message.failed` e o dono do canal recebe um e-mail.

Não reenvie do seu lado enquanto uma mensagem estiver `queued`. Se o seu próprio
cliente HTTP atingir o timeout antes da resposta, veja
[Tempo de resposta](#response-time).

---

## Recebendo Mensagens

Quando um usuário envia mensagem ao seu canal conectado, o Fiwano entrega a mensagem — e depois seus status de entrega — à `webhook_url` do seu canal como uma requisição `POST`. Esta página cobre a verificação de webhooks, os formatos de payload por canal, o download de mídia recebida e a consulta ao perfil de um remetente.

Defina a `webhook_url` e escolha quais `webhook_events` receber ao conectar um canal (veja [Canais](/br/documentation/channels)); por padrão nenhum evento está habilitado. Se o canal tiver um [`webhook_secret`](/br/documentation/channels#webhook-secret), cada entrega é assinada para você verificar que veio do Fiwano — fortemente recomendado. Enquanto você não definir um segredo, as entregas são enviadas sem assinatura.

### Setup e payload de webhook do WhatsApp

Para WhatsApp, habilite `message.received` no canal e aponte `webhook_url` para
seu endpoint HTTPS público. O envelope do webhook tem o mesmo formato nos três
canais:

- Remetentes do WhatsApp chegam como números de telefone em `data.from`.
- Remetentes do Instagram chegam como valores IGSID em `data.from`.
- Remetentes do Facebook Messenger chegam como valores PSID em `data.from`.

Os campos de nível superior (`event`, `channel_id`, `channel_type`, `timestamp`,
`data`) permanecem estáveis, então um único receiver consegue lidar com webhooks
do WhatsApp, webhooks do Instagram e webhooks do Messenger sem três parsers
separados da Meta.

### Verificando assinaturas

Quando o canal tem um `webhook_secret`, toda requisição de webhook inclui um header `X-Webhook-Signature`:

```
X-Webhook-Signature: sha256=<hmac_hex>
```

Para verificar: compute o `HMAC-SHA256` do corpo bruto (raw) da requisição usando seu `webhook_secret` como chave, depois compare o digest em hex. (Se nenhum segredo estiver configurado, esse header não é enviado — defina um para habilitar a verificação.)

```python
import hmac, hashlib

def verify_signature(body: bytes, secret: str, header: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    received = header.replace("sha256=", "")
    return hmac.compare_digest(expected, received)
```

### Tipos de evento

Cada tipo de canal suporta um conjunto específico de eventos de webhook. **Somente os eventos que você habilitar explicitamente via `webhook_events` são entregues.** Por padrão, nenhum evento está habilitado — você precisa configurá-los após conectar um canal.

| Evento | Descrição | Canais |
|---|---|---|
| `message.received` | Mensagem recebida de um usuário | Todos |
| `message.echo` | Cópia de uma mensagem que sua empresa enviou **fora da Fiwano** (app WhatsApp Business, caixa de entrada do Instagram, Caixa de Entrada da Página do Facebook, Meta Business Suite, outra integração) | Todos |
| `conversation.referral` | Um usuário que retorna clicou em um anúncio ou link m.me / ig.me em uma conversa existente sem escrever; traz o mesmo bloco [`referral`](#referral) de `message.received` e reabre a janela de 24 horas (beta) | Instagram, Facebook |
| `message.sent` | Sua mensagem foi aceita pela Meta | WhatsApp |
| `message.delivered` | Mensagem entregue no dispositivo do destinatário | WhatsApp, Instagram, Facebook |
| `message.read` | Mensagem lida pelo destinatário * | WhatsApp, Instagram, Facebook |
| `message.failed` | Sua mensagem não pôde ser entregue | WhatsApp, Instagram, Facebook |

\* `message.read` depende das configurações de privacidade do destinatário no WhatsApp e no Instagram — se ele tiver desativado as confirmações de leitura, o status `read` nunca chegará. Trate `delivered` como um estado terminal de sucesso.

Novos tipos de evento podem ser adicionados com o tempo; eles nunca são ativados em um canal existente até você incluí-los em `webhook_events`. Ignore campos de payload que você não conhece — veja [Compatibilidade](/br/documentation#compatibility).


### Rastreamento de status de entrega

Quando você envia texto, mídia ou um modelo do WhatsApp por qualquer endpoint de
envio, recebe um `message_id` (um UUID do Fiwano). Todo webhook de status dessa mensagem
traz o mesmo `message_id`, no mesmo formato em todos os canais.

- Progressão de status: `sent → delivered → read`. Cada status implica todos os anteriores.
- `data.recipient` é o identificador do usuário: número de telefone (WhatsApp), IGSID (Instagram) ou PSID (Facebook).
- **Cascata de leitura:** quando um usuário lê uma conversa, o Fiwano envia um webhook `message.read` separado para *cada* mensagem não lida que você enviou a esse usuário — não apenas a mais recente.
- Os status podem chegar fora de ordem — no Instagram um `read` pode chegar antes do `delivered` da mesma mensagem. Trate o status mais alto já visto como o atual e faça upsert por `message_id`.

### Formato do payload

Todos os payloads compartilham a mesma estrutura de nível superior:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": { ... }
}
```

#### message.received (WhatsApp)

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "text",
    "text": "Hello!"
  }
}
```

`data.from` — número de telefone do remetente sem `+`. Use diretamente como `recipient` ao responder.

#### message.received (Instagram)

```json
{
  "event": "message.received",
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "mid.xxx",
    "from": "6543217890123456",
    "from_name": null,
    "type": "text",
    "text": "Hi there!"
  }
}
```

`data.from` — IGSID. Use como `recipient` ao responder. `from_name` é sempre `null` (a Meta não inclui o nome do remetente nos webhooks do IG).

#### message.received (Facebook Messenger)

```json
{
  "event": "message.received",
  "channel_id": "c3f8a1b2e4d56789",
  "channel_type": "facebook",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "mid.xxx",
    "from": "7890123456789012",
    "from_name": null,
    "type": "text",
    "text": "Hello from Messenger!"
  }
}
```

`data.from` — PSID. Use como `recipient` ao responder. `from_name` é sempre `null` (a Meta não inclui o nome do remetente nos webhooks do FB).

#### message.received — mídia (Pro)

Com uma licença **Pro**, mensagens de mídia incluem o conteúdo do arquivo. O arquivo de mídia é baixado da Meta e armazenado temporariamente. Use o `download_url` para buscar o arquivo antes que ele expire.

Exemplo de imagem no WhatsApp:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "image",
    "caption": "Check this photo",
    "media": {
      "media_id": "m1b2c3d4e5f67890",
      "mime_type": "image/jpeg",
      "file_size": 245760,
      "filename": null,
      "sha256": "abc123...",
      "duration_ms": null,
      "download_url": "https://fiwano.com/api/v1/media/m1b2c3d4e5f67890",
      "expires_at": "2025-01-15T11:30:00Z"
    }
  }
}
```

Exemplo de mensagem de voz no WhatsApp:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "audio",
    "media": {
      "media_id": "m2b3c4d5e6f78901",
      "voice": true,
      "mime_type": "audio/ogg; codecs=opus",
      "file_size": 12345,
      "filename": null,
      "sha256": "def456...",
      "duration_ms": 5200,
      "download_url": "https://fiwano.com/api/v1/media/m2b3c4d5e6f78901",
      "expires_at": "2025-01-15T11:30:00Z"
    }
  }
}
```

Instagram e Facebook Messenger entregam o mesmo bloco `data.media`; apenas `data.from` muda (IGSID ou PSID em vez de um número de telefone).

`data.type` é o tipo da mensagem em todos os canais: `text`, `image`, `audio`, `video`, `document`, `share` ou `unsupported`. Faça o roteamento por ele. Se algum dia aparecer um tipo que sua integração não reconhece, trate-o como `unsupported` (veja [Compatibilidade](/br/documentation#compatibility)). Os quatro tipos de mídia são exatamente os valores aceitos como `media_type` de saída, então não é preciso mapear tipos para encaminhar uma mensagem de mídia — apenas o arquivo precisa ser re-hospedado, porque a `download_url` é autenticada (veja [Baixando mídia recebida](#downloading-inbound-media)).

**Figurinhas (stickers) chegam como `image`** em todos os canais, com `media.sticker: true` para diferenciá-las de fotos. Uma figurinha do WhatsApp é um arquivo WebP (`mime_type: "image/webp"`); uma figurinha do Messenger também traz o `media.sticker_id` persistente da Meta (`369239263222822` é o joinha). O Instagram não entrega figurinhas. Para devolver uma figurinha use [`media_type: "sticker"`](/br/documentation/sending-messages#stickers) — encaminhar um WebP como `image` é rejeitado pelo WhatsApp.

Instagram e Facebook Messenger também usam anexos para coisas que não são arquivo. Um post, reel ou menção em story compartilhado chega como [`type: "share"`](#shares); um pin de localização ou um cartão de produto chega como `type: "unsupported"`, sem bloco `data.media` — veja **tipo unsupported** abaixo.

Quando a Meta inclui texto junto com mídia, o Fiwano expõe esse texto como `data.caption` no evento de mídia. Mensagens somente de texto continuam usando `data.text`. Essa regra é igual para WhatsApp, Instagram e Facebook Messenger.

**Vários anexos:** cada anexo é entregue em seu próprio webhook `message.received` e em seu próprio `POST` HTTP; o Fiwano nunca envia um array de eventos de webhook. Os eventos são enviados na ordem dos anexos da Meta. O primeiro evento mantém o ID da mensagem da Meta e recebe a legenda, quando houver. Os eventos seguintes usam IDs determinísticos com `.2`, `.3` e assim por diante, sem repetir a legenda:

```text
mid.xxx       imagem + legenda
mid.xxx.2     imagem
mid.xxx.3     vídeo
```

Trate o `message_id` recebido como uma chave opaca de idempotência; não interprete o sufixo nem envie esse ID para a Meta. As tentativas de entrega continuam independentes por evento, então uma falha no endpoint do cliente ainda pode fazer uma parte posterior chegar antes da nova tentativa de uma parte anterior. Uma falha no download de mídia não elimina os demais anexos: seu evento contém `media.download_url: null` e `media.error`.

O Fiwano preserva o formato de arquivo original da Meta e não faz transcodificação de mídia. `media.mime_type` descreve os bytes do arquivo baixado, não a semântica da mensagem. Por exemplo, clipes em estilo de voz do Facebook Messenger costumam baixar como OGG/Opus (`audio/ogg`), enquanto mensagens de áudio do Instagram podem baixar como MP4 somente-áudio servido com `video/mp4`. Em ambos os casos o tipo da mensagem ainda é `data.type: "audio"`.

O `download_url` é autenticado; busque-o com sua `X-API-Key`. Não o passe diretamente como `media_url` de saída, porque a Meta não enviará o header da sua API key — re-hospede os bytes atrás de uma URL HTTPS pública ou assinada primeiro.

**Campos do payload de mídia:**

| Campo | Tipo | Descrição |
|---|---|---|
| `media_id` | string | ID do arquivo de mídia — use em `GET /api/v1/media/{media_id}` para baixar |
| `voice` | bool | Presente apenas para mensagens de voz do WhatsApp (`true`). Omitido para IG/FB porque a Meta não fornece um indicador de voz confiável ali. |
| `sticker` | bool | Presente apenas quando a imagem é uma figurinha (`true`) — WhatsApp (WebP) e Facebook Messenger. |
| `sticker_id` | string | Apenas figurinhas do Facebook Messenger — id persistente da figurinha na Meta. Devolva-a com `media_type: "sticker"`. |
| `mime_type` | string | Tipo MIME (ex.: `image/jpeg`, `audio/ogg; codecs=opus`) |
| `file_size` | int | Tamanho do arquivo em bytes |
| `filename` | string\|null | Nome de arquivo original (apenas documentos) |
| `sha256` | string\|null | Hash SHA-256 da Meta (apenas WhatsApp) |
| `duration_ms` | int\|null | Duração em milissegundos (apenas áudio/vídeo) |
| `download_url` | string\|null | URL de download autenticada. `null` se o download da Meta falhou. |
| `error` | string | Presente apenas quando o download falhou — descreve o erro |
| `expires_at` | string | Timestamp ISO 8601 — o arquivo é excluído após esse horário |

> **Observação:** Trate mensagens de voz como mensagens de áudio. `data.type: "audio"` é o valor estável entre canais para roteamento e encaminhamento. `media.voice` é uma dica opcional, exclusiva do WhatsApp, para UI/UX.

#### Baixando mídia recebida

Busque o arquivo em `data.media.download_url` (que é `GET /api/v1/media/{media_id}`) com sua `X-API-Key`:

```bash
curl https://fiwano.com/api/v1/media/m1b2c3d4e5f67890 \
  -H "X-API-Key: YOUR_API_KEY" \
  --output photo.jpg
```

A resposta são os bytes brutos do arquivo com o `Content-Type` original (e um nome de arquivo em `Content-Disposition` quando conhecido). Os arquivos expiram cerca de 60 minutos após o Fiwano buscá-los da Meta; `media.expires_at` é a referência definitiva. Baixe prontamente e re-hospede o que precisar manter; após a expiração a URL retorna `410 Gone`. Os tamanhos estão em [Capacidades](/br/documentation/capabilities#media-limits); os códigos de status na [Referência da API](/documentation/api).

#### message.received — toques em botões e escolhas de menu (todos os canais)

Quando um usuário escolhe uma das opções que você ofereceu, a escolha chega como uma mensagem `text` comum cujo `text` é o rótulo que ele viu — um botão de resposta rápida de template do WhatsApp, um botão de resposta interativo ou uma linha de lista do WhatsApp, uma resposta rápida do Instagram ou do Messenger, e os postbacks do Messenger / Instagram (Get Started, ice breakers, menu persistente, botões de template):

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:31:00Z",
  "data": {
    "message_id": "wamid.yyy",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "text",
    "text": "Confirmar",
    "reply_to": {"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}
  }
}
```

Não há evento separado nem id de máquina do botão: um único handler de texto cobre respostas digitadas e toques. A qual mensagem o botão pertencia está em [`reply_to`](#reply-to) — no WhatsApp um toque em botão de template sempre cita o envio do template, então `reply_to.message_id` é o UUID que você recebeu de `send-template`.

#### message.received — posts, reels e menções em story compartilhados

No Instagram e no Facebook Messenger um usuário pode compartilhar um post ou um reel na conversa, ou mencionar sua conta no story dele. Esses chegam como `type: "share"`:

```json
{
  "event": "message.received",
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "mid.xxx",
    "from": "6543217890123456",
    "from_name": null,
    "type": "share",
    "share_type": "post",
    "caption": "Pôr do sol no píer",
    "share": {"url": "https://www.instagram.com/p/ABC123/", "expires_at": null}
  }
}
```

| Campo | Descrição |
|---|---|
| `share_type` | `post` (um post compartilhado), `reel` (um reel compartilhado), `story_mention` (Instagram: o usuário mencionou você no story dele) |
| `share.url` | Link da Meta para o conteúdo compartilhado. O link de um post ou reel abre no Instagram / Facebook; uma menção em story aponta para a mídia do próprio story |
| `share.expires_at` | Somente `story_mention`: o story desaparece cerca de 24 horas após ser publicado (possivelmente antes). `null` para posts e reels |
| `caption` | A legenda do post ou reel compartilhado, quando a Meta a fornece. Ausente em menções em story |

O conteúdo compartilhado não é baixado e não há bloco `data.media`: um post pertence ao seu autor, e a Meta não permite que aplicativos armazenem a mídia de stories. Uma foto ou vídeo do próprio usuário enviado como mensagem continua sendo um evento `image` / `video` comum.

#### Respostas e mensagens citadas — `reply_to`

Quando um usuário responde a uma mensagem específica ("Responder" no WhatsApp, deslizar para responder no Instagram / Messenger, toque em botão de template do WhatsApp), o evento traz `reply_to` ao lado de `type`:

```json
"reply_to": {"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}
```

`reply_to.message_id` é **o id que você já tem** para a mensagem citada: o UUID do Fiwano se for uma mensagem que você enviou pelo Fiwano (ou recebeu como `message.echo`), ou o id do provedor de `data.message_id` se for uma mensagem anterior do usuário. Compare com os ids que você armazenou e saberá qual mensagem foi citada; não há um sinalizador de origem separado. Para uma mensagem com vários anexos, é o id da primeira parte. No caso raro de uma resposta chegar ao Fiwano antes de o envio citado ser registrado, o id do provedor é repassado como está.

Uma resposta do Instagram ao seu story traz o story em vez de um id de mensagem:

```json
"reply_to": {
  "story": {
    "id": "17900000000000009",
    "url": "https://lookaside.fbsbx.com/…/story.jpg",
    "expires_at": "2025-01-16T10:30:00Z",
    "link_url": "https://shop.example/promo"
  }
}
```

`story.id` é o id de mídia do seu story, `story.url` um link temporário para a mídia dele, `story.expires_at` o fim das suas 24 horas de vida, e `story.link_url` a figurinha de link que o usuário tocou, quando houve uma (caso contrário `null`).

`reply_to` está presente em `message.received` e em [`message.echo`](#message-echo) (um operador respondendo a uma mensagem específica), nos três canais e para todos os tipos de mensagem. Está ausente quando a mensagem não é uma resposta.

#### Contexto de referral — anúncios e links (beta)

Quando uma conversa começa a partir de um anúncio Click-to-WhatsApp, Click-to-Instagram ou Click-to-Messenger, ou de um link m.me / ig.me com parâmetro `ref`, a Meta anexa a atribuição ao primeiro evento recebido. A Fiwano a repassa como `data.referral` no `message.received` que segue o clique — normalmente a primeira mensagem da conversa; um toque em ice breaker ou Get Started no Instagram / Messenger chega como `type: "text"` e a traz da mesma forma. Em uma mensagem com vários anexos, ela está apenas na primeira parte.

> **Beta até novembro de 2026.** As quatro chaves normalizadas (`source`, `text`, `image_url`, `ref`) e o evento `conversation.referral` podem ser ajustados; `raw` tem garantia de permanecer exatamente como está, então tudo o que for construído sobre `raw` está seguro. Se você pretende depender das chaves normalizadas ou de `conversation.referral`, avise-nos em support@fiwano.com: se algo mudar, informaremos antes da mudança.

```json
"referral": {
  "source": "ad",
  "text": "Chat with us\nSummer Succulents are here!",
  "image_url": "https://scontent.xx.fbcdn.net/v/t45.1/...",
  "ref": null,
  "raw": {
    "source_url": "https://fb.me/3cr4Wqqkv",
    "source_id": "120226305854810726",
    "source_type": "ad",
    "headline": "Chat with us",
    "body": "Summer Succulents are here!",
    "media_type": "image",
    "image_url": "https://scontent.xx.fbcdn.net/v/t45.1/...",
    "ctwa_clid": "Aff-n8ZTODiE79d22KtAwQKj9e_mIEOOj27vDVwFjN80dp4...",
    "welcome_message": {"text": "Hi there! Let us know how we can help!"}
  }
}
```

| Campo | Significado |
|---|---|
| `source` | De onde o usuário veio. `ad` — anúncio pago (inclusive posicionamentos em Story); `link` — link m.me / ig.me com `ref`; `product` — página de produto da Loja do Instagram. O conjunto é aberto: outra origem informada pela Meta é repassada em minúsculas, `unknown` significa que a Meta não enviou origem. |
| `text` | O texto do anúncio que o usuário viu. WhatsApp: título e texto principal unidos por quebra de linha. Instagram / Messenger: o título do anúncio fornecido pela Meta. `null` para links. |
| `image_url` | O criativo como imagem: a foto de um anúncio de imagem ou a miniatura de um anúncio de vídeo. `null` quando a Meta não envia. |
| `ref` | O seu próprio marcador do parâmetro `ref` de um link m.me / ig.me ou de um anúncio do Instagram / Messenger. Sempre `null` no WhatsApp. |
| `raw` | O objeto de referral da Meta exatamente como recebido: `source_id` / `ad_id`, `source_url`, `post_id`, `ctwa_clid`, `headline` / `body` / `ad_title`, `welcome_message`, `flow_id`, `product`. Os nomes dos campos variam por canal; consulte a referência da Meta do seu canal. |

`text` e `image_url` foram pensados para ir direto ao seu modelo: o usuário está respondendo a um anúncio que dizia isto e tinha esta aparência.

**Um usuário que retorna — `conversation.referral` (Instagram, Messenger).** Quando um usuário que já conversou com você clica em um anúncio ou em um link m.me / ig.me sem escrever, a Meta envia a atribuição como um evento separado, sem mensagem. O clique reabre a janela de 24 horas, então você pode responder. Ative `conversation.referral` em `webhook_events` para recebê-lo; o payload é `data.from`, `data.from_name` e o mesmo bloco `referral`. O WhatsApp não tem equivalente: lá a atribuição sempre chega junto com uma mensagem.

```json
{
  "event": "conversation.referral",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "instagram",
  "timestamp": "2026-09-21T10:30:00Z",
  "data": {
    "from": "17841400000000777",
    "from_name": null,
    "referral": {"source": "link", "text": null, "image_url": null, "ref": "spring_promo", "raw": {"ref": "spring_promo", "source": "SHORTLINKS", "type": "OPEN_THREAD"}}
  }
}
```

O que esperar da Meta:

- **Uma única vez.** O bloco vem no evento que segue o clique e não se repete nas mensagens seguintes. Guarde-o na conversa quando chegar; a Fiwano não mantém histórico de mensagens.
- **Os links do criativo são temporários.** `image_url` e as URLs em `raw` são links públicos assinados da CDN da Meta, sem token. A Meta não documenta a validade: baixe a imagem quando o evento chegar se quiser mantê-la.
- **A atribuição pode ser incompleta.** A Meta omite `raw.ctwa_clid` em anúncios posicionados no Status do WhatsApp e pode omiti-lo em cliques vindos de um navegador, após o anúncio ser excluído ou quando o usuário fechou o contexto do anúncio antes de escrever. A ausência do id de clique não significa conversa orgânica.
- **Só anúncios e links com `ref` trazem atribuição.** Uma mensagem pelo botão do perfil, pelo link na bio do Instagram, por um link wa.me ou por QR code é uma mensagem comum, sem `referral`.
- **`raw.welcome_message.text` (WhatsApp)** é a saudação configurada no anúncio. O WhatsApp a mostra no chat antes de o usuário escrever; ela não é enviada pela API, então não há mensagem de saída nem eco dela.

A Fiwano não chama a Conversions API da Meta. Para atribuir uma venda a um anúncio Click-to-WhatsApp, guarde `raw.ctwa_clid` e `raw.source_id` no início da conversa e envie o evento de conversão você mesmo dentro da janela de 7 dias da Meta (`action_source: business_messaging`, `messaging_channel: whatsapp`, `user_data.ctwa_clid` sem hash, `user_data.whatsapp_business_account_id`). No Instagram e no Messenger use `raw.ad_id` nos seus próprios relatórios; a Meta não tem id de clique nesses canais.

#### message.received — tipo não suportado (todos os canais)

Uma mensagem chega como `type: "unsupported"` quando o Fiwano não pode entregar o conteúdo como arquivo. `unsupported_type` diz o que era, e não há bloco `data.media`. Existem dois motivos, e `upgrade_required` os distingue.

**Mídia em uma licença Starter.** O arquivo existe, mas seu plano não o inclui. `unsupported_type` é o tipo de mídia que o Pro teria entregue, e `upgrade_required` indica o plano que o libera:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "unsupported",
    "unsupported_type": "image",
    "upgrade_required": "pro"
  }
}
```

Faça upgrade pela página Billing no portal para receber o conteúdo completo de mídia.

**Conteúdo que não é um arquivo.** Nenhum plano entrega esses casos, então `upgrade_required` está ausente. `unsupported_type` carrega o nome que a própria Meta usa para o conteúdo:

| Canal | Valores de `unsupported_type` |
|---|---|
| WhatsApp | `location`, `contacts`, `order` (pedido de catálogo), `system` (por exemplo, troca de número), `edit` e `revoke` (mensagem editada ou apagada no app WhatsApp Business), `nfm_reply` (resposta de um WhatsApp Flow), `poll_creation`, `poll_update`, `gif`, `group_invite` e qualquer outro tipo que o WhatsApp informe |
| Instagram | `template` (um produto ou cartão compartilhado de um catálogo), `ephemeral` (foto ou vídeo de visualização única — a Meta não entrega o conteúdo), `unsupported` (o próprio Instagram não conseguiu entregar o conteúdo) |
| Facebook Messenger | `template`, `location`, `appointment_booking`, `fallback` (um link compartilhado que veio sem URL), `unsupported` |

Qualquer tipo não listado aqui chega da mesma forma, então um `unsupported_type` desconhecido continua sendo apenas conteúdo não suportado. Reações a mensagens, edições e exclusões de mensagens, e mensagens em grupos do WhatsApp não são entregues como eventos de webhook. Uma prévia de link do Messenger nunca vira `unsupported`: o texto da mensagem com o link é entregue como `text`, e um link encaminhado sem texto chega como `text` contendo a URL.

#### message.echo — mensagens enviadas fora da Fiwano

Quando alguém do seu lado responde a um cliente **sem passar pela Fiwano**, a Meta
ecoa essa mensagem de volta — e a Fiwano pode entregar uma cópia para você, para que
seu sistema veja a conversa inteira, não apenas a metade dele. Origens por canal:

| Canal | De onde a mensagem foi enviada |
|---|---|
| WhatsApp | App WhatsApp Business ou dispositivo vinculado, em um número Coexistence |
| Instagram | Caixa de entrada do app Instagram, Meta Business Suite ou outra integração |
| Facebook Messenger | Caixa de Entrada da Página, Meta Business Suite ou outra integração |

Habilite por canal adicionando `message.echo` a `webhook_events` (desativado por
padrão, disponível em todos os planos). Mensagens enviadas pela Fiwano nunca chegam
como eco — você já as tem.

```json
{
  "event": "message.echo",
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "timestamp": "2026-09-01T10:30:00Z",
  "data": {
    "message_id": "550e8400-e29b-41d4-a716-446655440000",
    "recipient": "6543217890123456",
    "status": "sent",
    "type": "text",
    "text": "Resposta do operador"
  }
}
```

- `message_id` — um UUID da Fiwano, exatamente como o que você recebe ao enviar
  pela API. Ele é **estável**: se a Meta reenviar o mesmo eco, você recebe o mesmo
  UUID — deduplique por ele.
- `recipient` — o usuário que recebeu a mensagem, no mesmo formato aceito pelos
  endpoints de envio (telefone no WhatsApp, IGSID no Instagram, PSID no Facebook).
  Você pode responder diretamente para `recipient`.
- `status: "sent"` — o estado inicial do ciclo de vida. Um eco confirma que a
  mensagem existe na conversa, não que chegou ao dispositivo do destinatário. Não é
  emitido `message.sent` separado para ecos.
- Quem exatamente enviou a mensagem (operador, dispositivo ou app) não é exposto —
  a Meta não fornece uma identidade confiável para isso.

**Rastreamento de status para ecos.** Por padrão um eco é uma cópia única: sem
`delivered`/`read` posteriores. Defina o campo booleano `echo_statuses` do canal
como `true` (via `PATCH /api/v1/channels/{id}` ou no Portal) e as mensagens ecoadas
ganham o mesmo ciclo de status das mensagens enviadas pela Fiwano: os webhooks
`message.delivered` / `message.read` / `message.failed` seguintes referenciam o
mesmo `message_id` do eco e são filtrados pelo seu `webhook_events` exatamente como
os status comuns.

Os status delivered e read de ecos do WhatsApp são entregues da mesma forma que
para mensagens enviadas pela Fiwano. A Meta não garante formalmente a entrega de
status para mensagens enviadas do app WhatsApp Business, então trate um status
ausente como normal, não como erro.

O Instagram não tem recibo de entrega; o próprio eco equivale ao `delivered`
sintético que a Fiwano emite para os seus envios no Instagram, então nenhum
`message.delivered` separado segue um eco do Instagram. Uma confirmação de
leitura do Instagram cobre a conversa inteira: segue um `message.read` para cada
mensagem ecoada que o usuário ainda não tinha lido, da mesma forma que para as
mensagens enviadas pela Fiwano.

Status e ecos são entregues de forma independente e at-least-once: um status pode
ocasionalmente chegar antes do eco ao qual pertence. Correlacione por `message_id`
e faça upsert em vez de depender da ordem de chegada.

**Mídia em ecos não é entregue.** Uma mensagem de mídia ecoada mantém o
`data.type` real (`image`, `audio`, `video`, `document`; uma figurinha é `image`
com `media.sticker: true`) e a legenda quando presente, mas o arquivo em si é
pulado — `data.media` chega sem download:

```json
{
  "data": {
    "message_id": "550e8400-e29b-41d4-a716-446655440000",
    "recipient": "6543217890123456",
    "status": "sent",
    "type": "image",
    "caption": "Foto da fatura",
    "media": {"media_id": null, "download_url": null, "unavailable": "echo_media_not_supported", "kind": "image"}
  }
}
```

A regra que você já aplica à mídia recebida — *verifique `media.download_url`
antes de baixar* — cobre este caso sem código extra e mantém seu handler
compatível caso a mídia de ecos fique disponível no futuro. Mensagens do
Instagram/Messenger com múltiplos anexos são divididas em eventos `message.echo`
separados por anexo (cada um com seu próprio `message_id`); um post ou reel
compartilhado chega como [`type: "share"`](#shares) e os demais anexos que não
são arquivos como `type: "unsupported"` com `unsupported_type` — igual ao
`message.received`. O eco de uma resposta traz [`reply_to`](#reply-to): quando
um operador responde a uma mensagem específica do cliente, `reply_to.message_id`
é o id do provedor dessa mensagem.

**Não são entregues como ecos:** reações, edições e exclusões de mensagens
(unsend). São alterações de uma mensagem existente, não mensagens novas, e são
puladas silenciosamente. No WhatsApp, ecos existem apenas para números
Coexistence — um canal conectado somente pela Cloud API não tem origem de
mensagens externas, então `message.echo` nunca dispara nele.

> **Atenção:** nunca espelhe um eco de volta na mesma conversa automaticamente.
> Sua resposta não geraria eco (envios da Fiwano são filtrados), mas um bot do
> outro lado — ou uma segunda integração que também espelha ecos — pode criar um
> loop. Sempre deduplique por `message_id` antes de agir sobre um eco.

#### message.delivered / message.read (todos os canais)

```json
{
  "event": "message.read",
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "timestamp": "2025-01-15T10:30:10Z",
  "data": {
    "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "read",
    "recipient": "6543217890123456"
  }
}
```

Mesmo formato para todos os canais e todos os status (`sent`, `delivered`, `read`). `message_id` é o UUID da resposta de envio.

#### message.failed (todos os canais)

```json
{
  "event": "message.failed",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:05Z",
  "data": {
    "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "recipient": "1234567890",
    "status": "failed",
    "error": "Message undeliverable",
    "errors": [{"code": 131047, "title": "Message undeliverable"}]
  }
}
```

Enviado quando uma mensagem que você enviou não é entregue **depois** que a
chamada de envio retornou:

- o envio retornou `queued` e a Fiwano não conseguiu concluí-lo — a Meta
  continuou rejeitando até as [novas tentativas](/br/documentation/sending-messages#delivery-and-retries)
  se esgotarem, rejeitou de forma permanente em uma nova tentativa, nunca o
  confirmou, ou o canal foi desconectado enquanto a mensagem aguardava;
- no WhatsApp, também quando a Meta aceitou a mensagem e informa depois que não
  conseguiu entregá-la.

Um envio que retorna `failed` imediatamente não gera webhook — a resposta já
informa isso. `error` é um motivo curto em texto, sempre presente. `errors` é um
array com os detalhes completos do erro da Meta — `code`, `title`, `message` e
`error_subcode` / `error_data.details` quando a Meta os fornece — presente só
quando a falha foi causada pela Meta. `message.failed` é definitivo: nenhum
outro status vem depois dele para o mesmo `message_id`.

### Política de novas tentativas

Se sua webhook URL retornar um status não-2xx ou estiver inacessível, o sistema tenta novamente automaticamente:

- **7 tentativas** com backoff exponencial: 30s, 1m, 2m, 2m, 2m, 2m, 2m (~12 minutos no total)
- **Prazo rígido de 20 minutos** — após o qual a entrega é marcada como permanentemente falha
- **E-mail de aviso** enviado após a 3ª tentativa falha (com as novas tentativas ainda em andamento)
- **Alerta por e-mail** enviado quando todas as tentativas se esgotam (falha permanente)
- Os payloads ficam armazenados, criptografados, apenas enquanto uma entrega está em nova tentativa, e são apagados quando ela é concluída ou expira
- No portal, a página **Delivery problems** do canal (aba Webhooks) lista as entregas dos últimos 7 dias que estão em nova tentativa, falharam ou se recuperaram após 3+ tentativas, com o erro do seu endpoint (os payloads nunca são exibidos)

**Importante:** Seu endpoint **deve responder com HTTP 2xx em até 5 segundos**. Respostas não-2xx ou timeouts disparam as novas tentativas acima.

**Responda primeiro, processe depois.** Faça o trabalho lento, como gerar uma resposta de IA, depois de retornar 2xx (em plataformas serverless, com o mecanismo de tarefas em segundo plano da plataforma). Uma resposta lenta é reenviada e entrega o mesmo evento de novo — deduplique `message.received` por `message_id`.

> **Dica:** Habilite apenas os eventos de webhook que você de fato trata. Eventos não tratados que recebem respostas não-2xx vão encher sua fila de novas tentativas desnecessariamente.

### Perfil do remetente

O WhatsApp inclui o nome do remetente em cada webhook (`data.from_name`) — nenhuma chamada extra necessária. Instagram e Facebook **não** (`data.from_name` é sempre `null`); para obter um nome ou avatar, chame o endpoint de perfil:

```
GET /api/v1/channels/{channel_id}/profile/{user_id}
```

Passe o valor de `data.from` (IGSID para Instagram, PSID para Facebook) como `user_id`. Ele retorna:

- **Instagram** — `username`, `name`, `profile_pic`, `follower_count`, `is_verified_user`
- **Facebook** — nome de exibição em `first_name`; `last_name` e `profile_pic` quando disponíveis na Meta

WhatsApp não é suportado (o nome já está no webhook). Os resultados são cacheados por até 5 minutos; a flag `cached` da resposta indica se foi um acerto de cache. Request/response completos e códigos de status estão na [Referência da API](/documentation/api).

> **Dica:** chame isto uma vez quando você vir um novo `data.from` pela primeira vez, depois cacheie o resultado do seu lado — não há necessidade de chamar a cada mensagem.

---

## Modelos do WhatsApp

O WhatsApp exige **modelos pré-aprovados** para iniciar uma conversa fora da
janela de 24 horas (veja [Capacidades](/br/documentation/capabilities#messaging-windows-24h)).
Modelos são exclusivos do WhatsApp e exigem uma **licença Pro**. Esta página trata de
gerenciá-los; para *enviar* um modelo aprovado, veja
[Enviando → Mensagens de modelo](/br/documentation/sending-messages#template-messages).

### Ciclo de vida

```
Create → PENDING (revisão da Meta, ~24h) → APPROVED (pronto para envio)
                                          → REJECTED (corrija e reenvie)
```

A Meta pode depois pausar (`PAUSED`, baixa qualidade) ou desativar (`DISABLED`)
um modelo aprovado. Um envio então falha com `error_code` `132015` / `132016`;
listar os modelos com a sincronização padrão mostra o status atual.

Os modelos pertencem à WhatsApp Business Account (WABA) do canal. Gerencie-os
por estes endpoints — os schemas completos de request/response estão na
[Referência da API](/documentation/api):

| Tarefa | Endpoint |
|---|---|
| Listar (filtra por status; sincroniza da Meta por padrão) | `GET /api/v1/channels/{id}/templates` |
| Obter um (componentes + definições de variáveis) | `GET /api/v1/channels/{id}/templates/{template_id}` |
| Criar (→ enviado à Meta, começa em `PENDING`) | `POST /api/v1/channels/{id}/templates` |
| Atualizar componentes | `PUT /api/v1/channels/{id}/templates/{template_id}` |
| Excluir | `DELETE /api/v1/channels/{id}/templates/{template_id}` |

### Criando um modelo

Um modelo é um `name` + `category` (`MARKETING`, `UTILITY` ou `AUTHENTICATION`)
+ `language` + `components`. `BODY` é obrigatório; `HEADER` (somente texto), `FOOTER` e
`BUTTONS` são opcionais. As variáveis são `{{1}}, {{2}}` (posicionais) ou `{{name}}`
(nomeadas) — a Meta exige valores de `example` para a revisão.

```bash
curl -X POST https://fiwano.com/api/v1/channels/a1b2c3d4e5f67890/templates \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "order_confirmation",
    "category": "UTILITY",
    "language": "en_US",
    "components": [
      {"type": "BODY", "text": "Hi {{1}}, your order {{2}} is confirmed.",
       "example": {"body_text": [["Pablo", "ORD-123"]]}}
    ],
    "parameter_format": "positional"
  }'
```

### Regras importantes

- **Editar um modelo aprovado** o reenvia para revisão (volta a `PENDING`)
  e é limitado pela Meta: **no máximo 10 edições a cada 30 dias, 1 a cada 24 horas**. Você
  não pode alterar a categoria de um modelo aprovado.
- **Excluir um modelo aprovado** bloqueia seu **nome por 30 dias** — você não pode
  recriar um modelo com o mesmo nome até lá (restrição da Meta).
- **Criar** é limitado a ~100 modelos por WABA por hora.

Quando um modelo está `APPROVED`, envie-o com
[`POST /api/v1/messages/send-template`](/br/documentation/sending-messages#template-messages).

### Botões

`BUTTONS` pode conter botões `URL`, de telefone e `QUICK_REPLY`. Quando um
destinatário toca em um botão de resposta rápida, seu webhook recebe um
[`message.received` comum de `type: "text"`](/br/documentation/webhooks#button-taps)
cujo `text` é o rótulo do botão, com `reply_to.message_id` igual ao
`message_id` que você recebeu de `send-template` — assim você sabe qual botão
foi escolhido e a qual envio ele responde. Mensagens interativas avulsas com
botões de resposta ou listas (fora de templates) ainda não podem ser enviadas,
mas os toques nelas são recebidos da mesma forma.

---

## Capacidades e Limites

O que cada canal suporta, os planos de licença e os limites da plataforma.

### Capacidades por canal

| Recurso | WhatsApp | Instagram | Facebook Messenger |
|---|---|---|---|
| Texto de saída — tamanho máximo | 4096 chars | 1000 chars | 2000 chars |
| Mídia de saída (Pro) | image, audio, video, document, sticker (arquivo WebP) | image, audio, video, document | image, audio, video, document, sticker (`sticker_id` do catálogo da Meta) |
| Mensagens de modelo (Pro) | ✅ Obrigatórias fora da janela de 24h | ❌ Não suportado | ❌ Não suportado |
| Webhooks recebidos — texto | ✅ `type: "text"` | ✅ `type: "text"` | ✅ `type: "text"` |
| Webhooks recebidos — mídia (Pro) | image, audio, video, document (figurinhas como `image` + `media.sticker`) | image, audio, video, document | image, audio, video, document (figurinhas como `image` + `media.sticker`) |
| Status de entrega | `sent` `delivered` `read` `failed` | `delivered` `read` `failed` | `delivered` `read` `failed` |
| Formato do destinatário | Número de telefone sem `+` | IGSID | PSID |
| Contorno da janela de 24h | Usar modelos aprovados | Nenhum — aguardar o usuário enviar mensagem | Nenhum — aguardar o usuário enviar mensagem |
| Identificador do canal | `phone_number_id` | `ig_account_id` | `page_id` |
| Perfil do remetente | `data.from_name` (dos contatos da Meta) | Via [endpoint de perfil](/br/documentation/webhooks#sender-profile) | Via [endpoint de perfil](/br/documentation/webhooks#sender-profile) |

> **Observação:** Cada conta Meta (número de telefone, conta do Instagram ou Página do Facebook) só pode estar conectada a uma conta Fiwano por vez.

### Planos de licença

O Fiwano oferece dois planos de licença. Cada canal conectado exige uma licença ativa.

| Plano | Mensal | Capacidades |
|---|---|---|
| **Starter** | US$ 12 | Mensagens de texto recebidas e enviadas ilimitadas, status de entrega |
| **Pro** | US$ 19 | Tudo do Starter **+** mídia recebida com arquivos, mídia enviada via URL HTTPS (URLs assinadas suportadas), gerenciamento e envio de modelos do WhatsApp |

Contas novas começam com um período de teste gratuito de 7 dias (plano Pro). Para o ciclo de cobrança e como o estado de assinatura de um canal é reportado, veja [Assinaturas e Cobrança](/br/documentation/subscriptions). Para entender como essa taxa fixa se relaciona com as tarifas por mensagem da própria Meta, veja [Custos de Mensagens Explicados](/br/documentation/messaging-costs).

### Limites de taxa

Os envios são limitados a **10 tentativas de envio aceitas por segundo por canal**,
somadas entre todas as API keys. Texto, mídia e modelos compartilham esse limite:
criar outra key não aumenta a capacidade de um canal, enquanto uma única key pode
operar vários canais de forma independente. Ao excedê-lo, a API retorna HTTP `429`
com `Retry-After`.

Todas as outras operações da API pública (canais, assinaturas, templates, perfis
do remetente, download de mídia, redirects) são limitadas a **20 requisições por
segundo por API key**. É uma proteção contra loops descontrolados, não uma cota:
uma integração normal fica muito abaixo disso, e uma key com problema não afeta as
outras keys da mesma conta. Ao excedê-lo, a API retorna HTTP `429` com
`Retry-After`. Faça leituras disparadas por eventos (download de mídia, perfil do
remetente) com nova tentativa em `429`, não em uma rajada paralela sem limite — a
mídia recebida fica disponível por 60 minutos.

Em saturação excepcional do envio, uma tentativa pode retornar brevemente HTTP
`503` + `Retry-After`; respeite o header e tente novamente. A Meta também aplica
limites próprios por canal e destinatário (fora do controle do Fiwano).

### Janelas de mensagem (24h)

A Meta restringe quando você pode enviar mensagem a um usuário fora de uma conversa aberta:

- **Instagram e Facebook Messenger** — você só pode responder dentro de **24 horas** da
  última mensagem do usuário. Essas respostas são **gratuitas** — a Meta não cobra
  por elas. Não há contorno por modelo — aguarde o usuário enviar mensagem novamente.
- **WhatsApp** — você só pode enviar texto comum dentro de **24 horas** da última
  mensagem do cliente. Fora da janela, use um modelo aprovado via
  `POST /api/v1/messages/send-template`. Isso é política da Meta. As respostas
  dentro da janela são **gratuitas nas primeiras 1.000 por mês por número**; a Meta
  cobra as respostas depois disso, e todos os modelos. Sem forma de pagamento na
  conta WhatsApp Business, as 1.000 gratuitas continuam sendo entregues, e as
  respostas seguintes não são entregues (erro `131042`) —
  veja [Custos de Mensagens](/br/documentation/messaging-costs).

### Limites de mídia

#### Tamanho de arquivo enviado

A Meta baixa sua `media_url` e aplica seus próprios limites por plataforma. O
Fiwano não reverifica o arquivo, então um arquivo acima do limite é rejeitado
pela Meta com `error_code` `100` e a mensagem **não** é repetida — veja
[Erros](/br/documentation/errors#send-error-codes).

| Tipo de mídia | WhatsApp | Instagram | Facebook Messenger |
|---|---|---|---|
| Imagem | 5 MB (JPEG, PNG) | 8 MB (JPEG, PNG) | 8 MB (JPEG, PNG, GIF) |
| Figurinha | 100 KB estática / 500 KB animada (WebP, 512×512 px) | não disponível | sem arquivo — enviada pelo `sticker_id` do catálogo da Meta |
| Vídeo | 16 MB (MP4, 3GPP) | 25 MB (MP4, OGG, AVI, MOV, WebM) | 25 MB |
| Áudio | 16 MB (AAC, AMR, MP3, MP4, OGG) | 25 MB (AAC, M4A, WAV, MP4) | 25 MB |
| Documento | 100 MB (PDF, Office, texto) | 25 MB (PDF) | 25 MB |

Estes são limites da Meta e podem mudar; a sobrecarga de codificação pode
ultrapassar o limite mesmo quando o tamanho em disco parece seguro. A Meta não
lista de forma completa os formatos aceitos por tipo, então trate a rejeição por
tamanho ou formato não suportado em vez de confiar em uma lista fixa.

#### Mídia recebida

- **Mídia recebida** (imagens, áudio, vídeo, documentos) fica armazenada por
  **60 minutos**. Baixe-a via `GET /api/v1/media/{media_id}` prontamente após o
  webhook. Tamanho máximo de arquivo: **10 MB**.
- **Licença Pro obrigatória** para enviar/receber mídia e usar modelos do WhatsApp. Com
  uma licença Starter, a mídia recebida chega como `type: "unsupported"` com
  `upgrade_required: "pro"`. Veja [Assinaturas e Cobrança](/br/documentation/subscriptions).

---

## Erros

O Fiwano informa um problema de uma de duas formas:

- **A requisição é recusada** com um status HTTP `4xx`/`5xx` — veja
  [Erros HTTP](#http-errors). Um `4xx` de um endpoint de envio significa que
  nenhuma mensagem foi enviada.
- **A mensagem falha.** Os endpoints de envio respondem `200` com
  `success: false` e `status: "failed"`, ou um envio que respondeu `queued`
  termina depois em um webhook [`message.failed`](/br/documentation/webhooks#event-types).
  Quando a causa é da Meta, o motivo está no [código de erro de envio](#send-error-codes).

### Erros HTTP

O corpo tem um campo `detail`, normalmente uma descrição legível:

```json
{ "detail": "Channel is inactive" }
```

Um erro de validação (`422`) lista os campos com problema em `detail`. As
rejeições que você pode querer tratar no código trazem um `detail` estruturado
com um `code` estável, uma `message` e normalmente um `hint`:

| `detail.code` | Status | Quando |
|---|---|---|
| `text_too_long` | `400` | O texto passa do limite do canal; `max_length` e `actual_length` vêm incluídos |
| `invalid_recipient` | `400` | `recipient` está vazio, não tem dígitos ou não é um PSID/IGSID numérico no Messenger/Instagram; `reason` é `empty`, `no_digits` ou `not_numeric` |
| `recipient_equals_sender` | `400` | Um envio de WhatsApp é endereçado ao próprio número do canal |
| `invalid_media_request` | `400` | Uma figurinha usa o campo errado para o canal; `reason` é `media_url_required` (WhatsApp), `sticker_id_required` (Messenger) ou `sticker_unsupported` (Instagram) |
| `no_free_slot` | `409` | Todos os slots de assinatura deste tipo de canal estão ocupados; `occupied_by` lista os canais que os ocupam |

| Status | Significado | O que fazer |
|---|---|---|
| `400` | A requisição não pode ser executada como foi enviada — por exemplo, o canal está inativo ou precisa ser reconectado | Leia `detail`; corrija a requisição ou [reconecte o canal](/br/documentation/channels#reconnecting-an-inactive-channel) |
| `401` | Chave de API ausente, malformada ou revogada | Envie uma chave válida — veja [Autenticação](/br/documentation#authentication) |
| `402` | Nenhuma assinatura ativa, ou o recurso exige Pro (mídia, modelos) e o canal está no Starter | Veja [Assinaturas e Cobrança](/br/documentation/subscriptions) |
| `403` | A conta está inativa, ou o código OAuth pertence a outra conta | Fale com o suporte se a conta deveria estar ativa |
| `404` | Não encontrado, ou pertence a outra conta | Verifique o ID |
| `409` | Nenhum slot de assinatura livre (`no_free_slot`) | Reconecte um canal, libere um slot ou adicione uma assinatura — veja [slots de assinatura](/br/documentation/channels#subscription-slots) |
| `410` | A mídia recebida expirou | Baixe em até 60 minutos — veja [Baixando mídia recebida](/br/documentation/webhooks#downloading-inbound-media) |
| `422` | Um campo está ausente, tem o tipo errado ou viola uma restrição | Leia a lista de campos em `detail` |
| `429` | Limite de taxa — por canal nos envios, por API key no restante | Tente novamente após `Retry-After` — veja [limites de taxa](/br/documentation/capabilities#rate-limits) |
| `502` | A Meta falhou em uma chamada de perfil do remetente ou de gestão de modelos (os endpoints de envio nunca o retornam) | Tente novamente mais tarde |
| `503` | Indisponível temporariamente | Tente novamente após `Retry-After` |

Não dependa do corpo de um `5xx`; use o status e `Retry-After`. Os códigos por
endpoint estão na [Referência da API](/documentation/api).

### Códigos de erro de envio

Quando a Meta rejeita uma mensagem, o código de erro dela chega a você sem
alteração:

- na resposta do envio, como `error_code` junto de `status: "failed"`;
- em um webhook `message.failed`, como `errors[].code`, com `error_subcode` e
  `error_data.details` quando a Meta os informou.

`error` traz o motivo legível: o texto da própria Meta, mais uma dica do Fiwano
quando o texto da Meta não explica o problema.

A coluna *Repetido* vale para envios de texto e mídia: esse envio responde
`queued` e o Fiwano tenta de novo — veja
[Entrega e novas tentativas](/br/documentation/sending-messages#delivery-and-retries).
`send-template` nunca repete: ali todo erro retorna `failed`, com o código da
Meta em `error_code`.

| Código | Significado | Repetido | O que fazer |
|---|---|---|---|
| `10` | A Meta recusa a ação. Há quatro causas sem relação entre si | não | Veja [Erro 10](#error-10) |
| `100` | Parâmetro inválido. A Meta o usa para causas distintas, incluindo **arquivo acima do [limite de tamanho](/br/documentation/capabilities#outbound-media-size)** e um PSID/IGSID que não é usuário desta Página | não | Leia `error`; verifique `media_url`, `media_type`, o tamanho do arquivo e `recipient` |
| `190` | O acesso do Fiwano à conta foi revogado ou expirou. O canal é marcado como *Action required* ([`health`](/br/documentation/channels#channel-health) na API) e o dono recebe um e-mail | não | [Reconecte o canal](/br/documentation/channels#reconnecting-an-inactive-channel) |
| `200` | WhatsApp: a Meta não permite que esta conta envie. Não é problema de token: o canal continua conectado e recebendo | não | Verifique a conta WhatsApp Business nas Configurações do Negócio da Meta |
| `368` | A conta está temporariamente bloqueada por violação de políticas | não | Resolva no Meta Business Manager |
| `551` | Messenger/Instagram: esta pessoa não está recebendo suas mensagens — bloqueou a Página, encerrou a conversa, restringiu mensagens de empresas ou nunca escreveu para a Página | não | Nada a corrigir do seu lado; não reenvie automaticamente |
| `803` | O destinatário não existe ou está indisponível | não | Verifique o ID do destinatário |
| `131008`, `131009` | Falta um parâmetro obrigatório, ou um valor não é válido para este canal | não | Corrija a requisição |
| `131026` | WhatsApp: não entregável — o número não está no WhatsApp, ou o destinatário não aceitou os termos do WhatsApp ou usa uma versão antiga do app | não | Verifique o número; não reenvie automaticamente |
| `131042` | Problema de pagamento na conta WhatsApp Business na Meta — sem forma de pagamento, linha de crédito acima do limite, conta suspensa. É a cobrança da Meta, não a sua assinatura Fiwano | **sim** | Corrija a cobrança da conta WhatsApp Business — veja o [artigo de ajuda da Meta](https://www.facebook.com/business/help/2225184664363779) |
| `131047` | WhatsApp: mais de 24 horas desde a última mensagem do cliente | não | Envie um [modelo](/br/documentation/sending-messages#template-messages) aprovado |
| `131049`, `131050`, `130472` | Modelo de marketing do WhatsApp não entregue: limite de marketing por usuário da Meta, o usuário parou de receber seu marketing, ou a Meta o reteve por um experimento | não | Não reenvie automaticamente |
| `131051` | Este tipo de mensagem não é suportado no canal | não | Veja as [capacidades do canal](/br/documentation/capabilities#channel-capabilities) |
| `131052` | A Meta não conseguiu baixar a `media_url` | não | A URL precisa responder `200` com um `Content-Type` igual ao `media_type`, e a assinatura precisa estar válida |
| `131053` | A Meta não conseguiu processar a mídia | **sim**, a menos que o próprio arquivo esteja errado | Geralmente temporário. Um WebP enviado como `image`, ou uma figurinha que viola as [regras de figurinhas](/br/documentation/sending-messages#stickers), falha na hora com uma dica |
| `131056` | WhatsApp: mensagens demais para o mesmo destinatário em pouco tempo | **sim** | Reduza o ritmo para esse destinatário |
| `131057` | Conta WhatsApp Business em modo de manutenção, por exemplo um upgrade de throughput | **sim** | Nada; é temporário |
| `132000`, `132001`, `132012`, `132015`, `132016` | Problema no modelo do WhatsApp: as variáveis não batem com o modelo (`132000`, `132012`), o modelo não existe ou não está aprovado neste idioma (`132001`), foi pausado (`132015`) ou desativado (`132016`) pela Meta | não | Corrija as variáveis ou verifique o modelo no WhatsApp Manager — veja [Modelos do WhatsApp](/br/documentation/templates) |
| `133010` | O número do WhatsApp não está registrado na WhatsApp Business Platform: a conexão pelo *WhatsApp Business App* não foi concluída | não | Reconecte escolhendo *WhatsApp Business App* e conclua a etapa no app — veja [Reconectando um canal](/br/documentation/channels#reconnecting-an-inactive-channel) |

Outros códigos chegam como a Meta os retorna, e o Fiwano repete qualquer código
que não saiba ser permanente. Listas completas da Meta:
[WhatsApp](https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes),
[Messenger e Instagram](https://developers.facebook.com/docs/messenger-platform/error-codes).

#### Erro `10`

A Meta retorna `10` para causas sem relação entre si. O texto em `error` as
diferencia:

| `error` contém | Causa | O que fazer |
|---|---|---|
| *outside of allowed window* | Messenger/Instagram: mais de 24 horas desde a última mensagem do usuário | Espere o usuário escrever de novo; não há alternativa com modelo — veja [janelas de mensagem](/br/documentation/capabilities#messaging-windows-24h) |
| *another app is controlling this thread* | Messenger/Instagram: o Fiwano não é o app no controle da conversa — outro app conectado, o inbox da Meta ou a Meta AI está | Torne o Fiwano o *Default routing app* — veja [Pré-requisitos](/br/documentation/channels#prerequisites) |
| *Meta has temporarily restricted this Page* (dica do Fiwano, sub-code `1893063`) | Messenger/Instagram: a Meta restringiu o envio pela Página por atividade contrária às suas políticas de mensagens. Alguns envios ainda podem passar | Verifique a Página em Qualidade da conta (Account Quality) no Meta Business Suite. Reconectar não resolve; quem remove a restrição é a Meta |
| qualquer outra coisa | A Meta nega esta ação para a conta | Leia `error` e verifique a conta nas Configurações do Negócio da Meta. Não é problema de token: o canal continua conectado |

---

## Assinaturas e Cobrança

Todo canal retornado por `GET /api/v1/channels` traz um objeto `subscription`
descrevendo seu estado de cobrança atual. Esta página explica o que esses estados
significam e como mudam ao longo do ciclo de vida de um canal. Para os tipos dos
campos, veja a **[Referência da API](/documentation/api)**.

Um canal pode **enviar e receber mensagens apenas enquanto sua assinatura estiver
`active`.** Quando não está, as chamadas de envio/recebimento são rejeitadas até que
uma assinatura seja (re)vinculada.

**Nomenclatura:** a API chama de *subscription* (assinatura); a página Billing do
portal chama a mesma coisa de *license*. Uma license = uma assinatura = um slot por
tipo de canal. Trial, Paddle e Enterprise são todos assinaturas aqui.

### O objeto subscription

```json
"subscription": {
  "status": "active",
  "source": "paddle",
  "tier": "pro",
  "expires_at": "2025-02-15T10:30:00",
  "auto_renew": true
}
```

- **`status`** — `active`, `expired`, `canceled` ou `none` (nenhuma assinatura vinculada;
  o canal não pode enviar/receber).
- **`source`** — de onde veio o direito de uso: `trial` (concedido automaticamente no
  cadastro), `paddle` (assinatura paga) ou `enterprise` (assinatura customizada
  provisionada pela equipe Fiwano, por exemplo um acordo de parceria ou cobrança por
  fatura). `null` quando `status` é `none`.
- **`tier`** — `starter` ou `pro`. `pro` é obrigatório para mensagens de mídia e
  CRUD/envio de modelos do WhatsApp. `null` quando `status` é `none`.
- **`expires_at`** — timestamp ISO-8601 UTC de quando o período atual termina. Se
  `auto_renew` for `true`, esta é a próxima data de renovação; caso contrário, é o
  limite após o qual o canal para de funcionar.
- **`auto_renew`** — `true` somente para uma assinatura Paddle ativa que será renovada
  em `expires_at`. Sempre `false` para trial e Enterprise.

### O que as combinações significam

- **Assinatura Paddle ativa** —
  `{status: "active", source: "paddle", auto_renew: true, expires_at: <próxima renovação>}`.
- **Renovação Paddle em nova tentativa** —
  `{status: "active", source: "paddle", auto_renew: true, expires_at: <há pouco no passado>}`.
  Enquanto um pagamento de renovação é retentado, `status` permanece `active` e `expires_at`
  pode ficar ligeiramente no passado — **o serviço continua durante essa breve janela de
  tolerância.** Depois resolve para renovado (`expires_at` futuro) ou, se o pagamento seguir
  falhando, expira.
- **Paddle com cancelamento agendado** —
  `{status: "active", source: "paddle", auto_renew: false, expires_at: <limite>}`.
  O cliente cancelou no Paddle; o serviço continua até `expires_at`, depois a assinatura
  termina e o canal deixa de enviar e receber.
- **Trial** —
  `{status: "active", source: "trial", tier: "pro", auto_renew: false, expires_at: <cadastro + 7 dias>}`.
- **Enterprise** —
  `{status: "active", source: "enterprise", auto_renew: false, expires_at: <fim do prazo acordado>}`.
  As renovações são combinadas com a equipe Fiwano antes de `expires_at`.
- **Sem assinatura ativa** —
  `{status: "none", source: null, tier: null, expires_at: null, auto_renew: false}`.
  Envio/recebimento falharão; vincule uma assinatura para restaurar o serviço.

> **Dica.** Trate `status` como a única fonte de verdade para saber se um canal pode
> operar. Não o deduza por conta própria a partir de `expires_at` — durante a janela de
> tolerância do Paddle, um canal `active` pode legitimamente ter um `expires_at` no passado.

### Verificando slots disponíveis

Use `GET /api/v1/subscriptions` quando um serviço externo precisar decidir se
pode iniciar um novo fluxo de conexão de canal. O endpoint é somente leitura e
retorna todas as assinaturas mais a disponibilidade agregada de slots:

```bash
curl -H "X-API-Key: $FIWANO_API_KEY" \
  https://fiwano.com/api/v1/subscriptions
```

```json
{
  "available_slots": {
    "whatsapp": { "total": 0, "starter": 0, "pro": 0 },
    "instagram": { "total": 1, "starter": 0, "pro": 1 },
    "facebook": { "total": 1, "starter": 0, "pro": 1 }
  },
  "subscriptions": [
    {
      "id": "a1b2c3d4e5f67890",
      "status": "active",
      "source": "trial",
      "tier": "pro",
      "auto_renew": false,
      "assigned_channels": {
        "whatsapp": {
          "channel_id": "1111222233334444",
          "channel_type": "whatsapp",
          "name": "Acme Support",
          "is_active": false
        },
        "instagram": null,
        "facebook": null
      }
    }
  ]
}
```

Use `available_slots.<channel_type>.total > 0` como o sinal de que um novo canal
desse tipo pode ser conectado. Um canal desativado continua ocupando seu slot
(veja [slots de assinatura](/br/documentation/channels#subscription-slots)). Em `available_slots`, `total` é a
soma dos slots `starter` e `pro` atualmente livres para esse tipo de canal. Em
cada assinatura, `assigned_channels` mostra qual canal está atribuído à
assinatura para cada tipo; `null` significa que nenhum canal está atribuído ali.
O mapeamento inverso está no próprio canal: `subscription.id` em
`GET /api/v1/channels`.

Para mover um canal para outra assinatura, ou liberar um slot para que outro
canal do mesmo tipo possa ocupá-lo, use `subscription_id` em
`PATCH /api/v1/channels/{channel_id}` — veja
[slots de assinatura](/br/documentation/channels#subscription-slots).

---

## Integração n8n

O Fiwano é um **node comunitário verificado do n8n** — listado em [n8n.io/integrations/fiwano/](https://n8n.io/integrations/fiwano/). Use-o para construir automações de WhatsApp, Instagram e Facebook Messenger, fluxos com agentes de IA e chatbots.

### Instalação

#### Pelo editor do n8n (recomendado)

1. Abra o painel de nodes com **+** ou **N**
2. Pesquise por **Fiwano**
3. Selecione **Fiwano** em **More from the community**
4. Clique em **Install**

No n8n Cloud, a instalação pode precisar ser habilitada antes pelo dono da instância no Cloud Admin Panel.

#### Alternativa manual (npm)

Use apenas quando a instalação pelo app não estiver disponível no seu ambiente (por exemplo, um setup self-hosted restrito).

```bash
mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes
npm install n8n-nodes-fiwano
# Reinicie o n8n
```

Para Docker self-hosted: inclua este pacote em uma imagem n8n customizada — veja o [repositório no GitHub](https://github.com/fiwano-com/n8n-nodes-fiwano) para detalhes.

### Nodes

| Node | Tipo | Descrição |
|---|---|---|
| **Fiwano** | Action | Enviar mensagens, gerenciar canais, modelos do WhatsApp, enriquecimento de perfil de contato, redirect URIs |
| **Fiwano Trigger** | Webhook Trigger | Receber mensagens recebidas e webhooks de status de entrega com verificação de assinatura HMAC opcional |

### Node Action — operações

| Recurso | Operações |
|---|---|
| Message | Send Text, Send Template (WhatsApp), Send Media (image/audio/video/document) |
| Media | Download (baixa um arquivo de mídia recebido; expira 60 min após o webhook) |
| Channel | Get Many, Get, Generate OAuth URL, Exchange OAuth Code, Update (configurações de webhook, rastreamento de status de echo e vínculo de assinatura), Deactivate |
| Subscription | Get Many (assinaturas, o canal atribuído a cada slot e os slots livres por tipo de canal e tier) |
| Contact | Get Profile (Instagram e Facebook — retorna nome/username e foto de perfil; Instagram também número de seguidores) |
| Template | Get Many, Get, Create, Update, Delete (somente WhatsApp) |
| Redirect URI | Get Many, Add, Delete |

**Deactivate é uma exclusão lógica.** Ela impede o canal de enviar e receber, mas preserva seu ID, seu histórico e seu slot de assinatura, para que a mesma conta Meta possa ser reconectada depois. Para liberar o slot para outro canal, desative-o e envie um **Subscription ID** vazio em **Update** — veja [slots de assinatura](/br/documentation/channels#subscription-slots).

### Node Trigger — eventos

Inicia seu workflow para qualquer um destes eventos (filtre por tipo de evento nas configurações do node). O que cada evento traz e quando dispara é o mesmo que sem n8n — veja [Receiving Messages → Event Types](/br/documentation/webhooks#event-types).

| Evento | Canais |
|---|---|
| `message.received` | WhatsApp, Instagram, Facebook |
| `message.echo` | WhatsApp (apenas Coexistence), Instagram, Facebook |
| `message.delivered` | WhatsApp, Instagram, Facebook |
| `message.read` | WhatsApp, Instagram, Facebook |
| `message.sent` | WhatsApp |
| `message.failed` | WhatsApp, Instagram, Facebook |
| `conversation.referral` (beta) | Instagram, Facebook |

No node:

- **Track Echo Statuses** (em **Channel → Update** e **Exchange OAuth Code**) é o `echo_statuses` da API — veja [message.echo](/br/documentation/webhooks#message-echo).
- O contexto de referral (beta) é `{{ $json.data.referral }}`; `data.referral.text` e `data.referral.image_url` podem ir direto para o prompt de um AI Agent — veja [Contexto de referral](/br/documentation/webhooks#referral).

### Padrões comuns de workflow

O node Fiwano é propositalmente pequeno: ele dá ao n8n uma camada de transporte
confiável para WhatsApp, Instagram DM e Facebook Messenger, e deixa a lógica do
workflow para o n8n e as ferramentas que você conecta ao redor.

| Padrão | Como montar |
|---|---|
| Agente de IA ou chatbot de WhatsApp | `Fiwano Trigger` recebe `message.received` → seus nodes de IA/modelo/ferramentas decidem a resposta → `Fiwano` envia a resposta. Use templates do WhatsApp apenas quando precisar iniciar ou reabrir uma conversa fora da janela de 24 horas. |
| Trigger n8n para WhatsApp | Use `Fiwano Trigger` com **Specific Channel** para um número de WhatsApp, ou **All Active Channels** quando um workflow deve atender todos os canais conectados. |
| Automação de Instagram DM | Use o mesmo par trigger/action em um canal de Instagram. Mantenha o workflow focado em atendimento recebido, qualificação de leads com opt-in e respostas a clientes; não construa scraping de cold-DM nem automação de spam. |
| Workflow de Facebook Messenger | Use ramificações por `channel_type: "facebook"` quando uma Página do Messenger precisar de texto ou roteamento diferente. O volume é menor que o do WhatsApp, mas é útil quando os clientes já começam pelo Messenger. |
| Um workflow para todos os canais Meta | Use **All Active Channels** e depois ramifique por `channel_type` (`whatsapp`, `instagram`, `facebook`) apenas onde as regras do canal forem diferentes. |

### Quando usar a Fiwano com n8n

Use quando você quer que o n8n seja dono da automação — lógica de IA, roteamento,
atualizações no CRM, memória, aprovações, escalonamento — e só precisa de uma forma
limpa de receber e enviar mensagens nos canais oficiais da Meta.

Não use como motor de disparo frio em massa. WhatsApp, Instagram e Messenger têm
regras de janela de mensagem e opt-in; a Fiwano segue as APIs oficiais e não contorna
as políticas da Meta.

### Configuração automática do webhook

O trigger pode configurar o webhook nos seus canais sozinho, sem você chamar **Update** manualmente. Escolha um modo de **Webhook Auto-Setup** e anexe uma credencial da API Fiwano. Os modos automáticos (**All Active Channels** / **Specific Channel**) precisam dela para chamar a API — se estiver faltando, a ativação falha com um erro claro. No **Manual** a credencial é opcional, usada apenas para ler um segredo de webhook padrão:

| Modo | Ao ativar o fluxo | Ao desativar |
|---|---|---|
| **All Active Channels** | Aponta para este trigger todo canal ativo que **ainda não esteja apontando para outro lugar** (WhatsApp + Instagram + Facebook) — um fluxo atende aos três. Canais que já apontam para outra URL são **deixados intactos**. | Limpa o webhook nos canais que ainda apontam para este trigger. |
| **Specific Channel** | Aponta um **Channel ID** para este trigger — **assume o canal** mesmo que ele já tenha um webhook. | Limpa o webhook desse canal (apenas se ainda apontar para cá). |
| **Manual** *(padrão)* | Nada — você define o `webhook_url` via **Exchange OAuth Code** / **Update**. Sem credencial. | Nada. |

Os **Event Types** selecionados no trigger são registrados como `webhook_events` do canal (eventos que não se aplicam ao tipo de canal são ignorados — ex.: `message.sent` no Instagram). Os canais começam sem eventos habilitados, então o auto-setup os ativa para você.

**Segredo do webhook.** Defina um **Webhook Secret** para verificar as assinaturas recebidas (HMAC-SHA256; divergências são rejeitadas com HTTP 401) e, no auto-setup, registrá-lo nos seus canais. Você pode defini-lo em dois lugares: o campo **Webhook Secret** do próprio trigger, ou — para reutilizar um único segredo em tudo — o campo **Webhook Secret** da credencial da API Fiwano. O campo do trigger tem prioridade; se estiver vazio, usa-se o segredo da credencial. Esse mesmo segredo da credencial também é usado pelas operações **Exchange OAuth Code** e **Update** quando você deixa o segredo delas vazio. Deixe ambos vazios para pular a verificação (não recomendado em produção).

**Quando roda:** apenas na **ativação / desativação** do fluxo (e quando o n8n reinicia fluxos ativos) — **nunca por mensagem**, portanto não adiciona custo ao processamento de mensagens. Pontos a observar:

- **Desativar remove o webhook** dos canais que apontam para este trigger. Isso apenas limpa a URL do webhook — **não** apaga o canal nem os dados existentes. Enquanto desativado, novos eventos de webhook recebidos não são repassados nem armazenados; reative para retomar a entrega.
- **O All Active Channels pula canais silenciosamente.** Um canal que já aponta para outra URL é deixado intacto e o fluxo ativa sem erro. Então, se um canal não estiver respondendo, verifique se o webhook dele aponta para outro lugar — limpe-o ou use **Specific Channel** para assumi-lo.
- **Limpe antes de remover.** Desative o fluxo (não apenas o exclua, e não remova a credencial antes) para o trigger conseguir limpar o webhook. Se a limpeza não rodar, um canal continua apontando para uma URL do n8n inativa — a Fiwano então registra falhas de entrega e envia e-mails até você limpá-lo (via **Update** ou pelo portal).
- Conectou um **canal novo** depois de ativar? Reative o fluxo (desligue/ligue) para o trigger configurá-lo.
- Dois fluxos **All Active Channels** não disputam um canal — quem reivindicar primeiro um canal livre fica com ele; o outro o deixa intacto. Para mover um canal de propósito, limpe o webhook dele ou use **Specific Channel**.
- Seu n8n precisa estar **acessível publicamente** — a Fiwano entrega webhooks pela internet na URL que o trigger registra.

### Workflows de exemplo

Dois workflows prontos para importar estão no [repositório no GitHub](https://github.com/fiwano-com/n8n-nodes-fiwano/tree/main/workflows). Use-os nesta ordem:

1. **Connect a Channel** — gera um link de conexão Meta por canal e captura o `channel_id` conectado automaticamente via um callback de webhook. (Você também pode conectar canais no [portal Fiwano](https://fiwano.com).)
2. **Universal Auto-Responder** — um único trigger responde a **todas** as mensagens no WhatsApp, Instagram e Facebook: ecoa texto e responde a anexos com os detalhes do arquivo. O padrão de ping-pong unificado — **precisa de pelo menos um canal conectado** (passo 1).

Importe pelo editor (**Workflows → Import from File…**) ou pela CLI.

---

## API Reference (OpenAPI)

The complete machine-readable contract for the public `/api/v1` API, generated from the live service (also at https://fiwano.com/api/v1/openapi.json and https://fiwano.com/api/v1/openapi.yaml).

```yaml
openapi: 3.1.0
info:
  title: Fiwano API
  description: 'Unified REST API for WhatsApp, Instagram and Facebook Messenger.


    Authenticate every request with the `X-API-Key` header (keys start with `mip_live_`,
    created on the API Keys page in the portal); `Authorization: Bearer <key>` is
    accepted when `X-API-Key` is absent. Keys are secret: call the API from your server,
    not from browser code. Base URL: `https://fiwano.com`.


    Human-readable guides: https://fiwano.com/documentation'
  version: 2.0.0
servers:
- url: https://fiwano.com
paths:
  /api/v1/subscriptions:
    get:
      tags:
      - api
      summary: List Subscriptions
      description: 'List subscriptions and free channel slots for the authenticated
        user.


        Use this endpoint before starting a channel setup flow to check whether a

        new WhatsApp, Instagram, or Facebook channel can be connected right now.

        A positive `available_slots.<type>.total` means a new channel can be added.

        POST /api/v1/channels/setup-url also permits reconnecting an existing

        inactive channel that still has an operable reserved slot.'
      operationId: list_subscriptions_api_v1_subscriptions_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionsResponse'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/channels:
    get:
      tags:
      - api
      summary: List Channels
      description: 'List all channels for the authenticated user.


        Returns both active and inactive channels. Each channel includes its current

        `subscription` (billing) state and the channel-type-specific identifiers

        (WhatsApp: phone_number_id/waba_id; Instagram: ig_account_id/ig_username;

        Instagram & Facebook: page_id).'
      operationId: list_channels_api_v1_channels_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelListResponse'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/channels/{channel_id}:
    get:
      tags:
      - api
      summary: Get Channel
      description: 'Get one channel by ID.


        Same shape as the list endpoint — a single channel object including its

        `subscription` (billing) state. Returns 404 if the channel does not belong

        to the authenticated account.'
      operationId: get_channel_api_v1_channels__channel_id__get
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    patch:
      tags:
      - api
      summary: Update Channel
      description: 'Update channel webhook settings, or move the channel between subscriptions.


        Set webhook_url to receive incoming messages. Must use HTTPS and cannot target
        loopback.

        Optionally provide a custom webhook_secret for HMAC verification.


        `subscription_id` binds the channel to another subscription, or — when sent
        as

        an empty string — releases its subscription slot so a different channel of
        the

        same type can be connected. Releasing a slot requires the channel to be

        deactivated first via DELETE /api/v1/channels/{channel_id}; this is deliberate,

        so a slot is never freed as a side effect of an unrelated settings update.


        Fields left out (or sent as null) are not changed, and the whole request is

        applied atomically: if the subscription change is rejected, the webhook settings

        are not updated either.'
      operationId: update_channel_api_v1_channels__channel_id__patch
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelUpdateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelUpdateResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api
      summary: Deactivate channel
      description: 'Deactivate a channel (soft delete).


        The channel stops sending and receiving, but is not erased — its channel_id,

        history and ownership claim are preserved so the same Fiwano account can

        reconnect it later via a new OAuth flow. Another Fiwano account cannot claim

        the Meta identity after deactivation; ownership release requires support.

        Fiwano unsubscribes the channel''s Meta webhook resource only when safe:

        a WABA subscription is kept if another active WhatsApp channel uses the same

        WABA, and a Page subscription is kept if another active Instagram/Facebook

        channel uses the same Page.'
      operationId: delete_channel_api_v1_channels__channel_id__delete
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/setup-url:
    post:
      tags:
      - api
      summary: Create Setup Url
      description: 'Generate a URL for connecting or reconnecting a channel on behalf
        of a user.


        The client opens the returned `setup_url` in a popup or browser. After the

        user completes Meta OAuth (or WhatsApp Embedded Signup), they are redirected

        to `redirect_uri` with a one-time `code`. Exchange that code via

        POST /api/v1/channels/exchange-code to obtain the channel_id.


        `redirect_uri` must already be whitelisted for this API key (add it via

        POST /api/v1/redirects), otherwise the request is rejected. The setup URL
        is

        valid until the `expires_at` returned in the response.


        There is intentionally no separate API reconnect endpoint. When Meta returns

        the identity of an existing channel of yours, setup updates that same

        channel row and returns its existing channel_id: an inactive channel is

        reactivated, an active one gets its credentials refreshed in place (this is

        how a channel that lost Meta access is repaired). A genuinely new eligible

        identity is preferred when both new and inactive assets are available.


        Refused with 402 when the account has no active subscription, and with 409

        (`detail.code` = `no_free_slot`, `detail.occupied_by` = channel ids) when

        every subscription slot of this type is already taken and none of the

        occupying channels can be reconnected through this flow.'
      operationId: create_setup_url_api_v1_channels_setup_url_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupUrlRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetupUrlResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/channels/exchange-code:
    post:
      tags:
      - api
      summary: Exchange Code
      description: 'Exchange a one-time completion code for channel data.


        After the user completes the OAuth flow, the redirect_uri receives a `code`
        parameter.

        This endpoint exchanges that code for the channel_id and basic details.


        The code is one-time use and expires after 5 minutes.'
      operationId: exchange_code_api_v1_channels_exchange_code_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeCodeRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeCodeResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/channels/{channel_id}/profile/{user_id}:
    get:
      tags:
      - api
      summary: Get Sender Profile
      description: "Fetch sender profile from Meta Graph API.\n\nReturns profile data\
        \ for a user who messaged your channel.\nSuccessful results are cached for\
        \ 5 minutes to reduce Meta API calls.\nUnavailable results are cached briefly\
        \ so newly indexed conversations recover quickly.\n\n- Instagram: username,\
        \ name, profile_pic, follower_count, is_verified_user\n- Facebook: display\
        \ name in first_name; last_name and profile_pic when available\n- WhatsApp:\
        \ not supported (name comes inline in webhooks via data.from_name)\n\nArgs:\n\
        \    channel_id: Channel ID\n    user_id: Sender identifier (IGSID for Instagram,\
        \ PSID for Facebook)\n\nReturns:\n    SenderProfileResponse with profile data\n\
        \nRaises:\n    HTTPException 404: Channel not found or WhatsApp (not supported)\n\
        \    HTTPException 400: Channel inactive or missing token\n    HTTPException\
        \ 502: Meta API error"
      operationId: get_sender_profile_api_v1_channels__channel_id__profile__user_id__get
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: user_id
        in: path
        required: true
        schema:
          type: string
          title: User Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SenderProfileResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/redirects:
    get:
      tags:
      - api
      summary: List Redirects
      description: 'List the redirect URIs whitelisted for this API key.


        These gate the programmatic channel-connection flow — the `redirect_uri` in

        POST /api/v1/channels/setup-url must match one of these patterns.'
      operationId: list_redirects_api_v1_redirects_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectListResponse'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
    post:
      tags:
      - api
      summary: Add Redirect
      description: 'Add an allowed redirect URI.


        Must be HTTPS. Explicit ports are supported. Exact URIs are preferred;

        constrained wildcard patterns like https://*.example.com/* remain supported.'
      operationId: add_redirect_api_v1_redirects_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectCreateRequest'
        required: true
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectCreateResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/redirects/{redirect_id}:
    delete:
      tags:
      - api
      summary: Remove Redirect
      description: Remove an allowed redirect URI.
      operationId: remove_redirect_api_v1_redirects__redirect_id__delete
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: redirect_id
        in: path
        required: true
        schema:
          type: string
          title: Redirect Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/messages/send:
    post:
      tags:
      - api
      summary: Send Message
      description: 'Send a text message through a connected channel.


        Recipient format is a phone number without `+` for WhatsApp, an IGSID for

        Instagram, and a PSID for Messenger. Text must be non-blank and stay within

        the platform limit. Transient failures are queued for background retry;

        permanent errors return `success: false` and are not retried. Two caller

        mistakes are rejected before reaching Meta with `400` and a structured

        detail: `invalid_recipient` (recipient empty, without digits, or not a

        numeric PSID/IGSID on Instagram/Facebook; `reason` and `hint` included) and

        `recipient_equals_sender` (a WhatsApp send addressed to the channel''s own

        number). Surrounding whitespace in `recipient` is trimmed.'
      operationId: send_message_api_v1_messages_send_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/media/{media_id}:
    get:
      tags:
      - api
      summary: Download Media
      description: 'Download a media file previously received via webhook.


        Requires a Pro license. Files are temporary and expire after

        the configured TTL (default 60 minutes).


        Response: raw file bytes with appropriate Content-Type header.'
      operationId: download_media_api_v1_media__media_id__get
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: media_id
        in: path
        required: true
        schema:
          type: string
          title: Media Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/messages/send-media:
    post:
      tags:
      - api
      summary: Send Media Message
      description: 'Send a media message through a connected Pro channel.


        Meta fetches `media_url` directly; Fiwano does not store the outbound file.

        Use a signed URL for non-public content and keep it valid for the retry

        window. Supported types are image, audio, video, document, and sticker

        (WhatsApp: WebP `media_url`; Facebook Messenger: `sticker_id`; Instagram:

        not available — `400 invalid_media_request`). Transient failures are

        queued; permanent payload or policy errors are not retried. `recipient`

        follows the same preflight as the text endpoint: trimmed, then

        `400 invalid_recipient` / `recipient_equals_sender` before any Meta call.'
      operationId: send_media_message_api_v1_messages_send_media_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMediaRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /api/v1/channels/{channel_id}/templates:
    get:
      tags:
      - api
      summary: List Templates
      description: 'List message templates for a WhatsApp channel.


        By default, syncs templates from Meta before returning (sync=true).

        Set sync=false to return cached data only (faster, but may be stale).


        Templates are tied to the WhatsApp Business Account (WABA).

        Only WhatsApp channels support templates.'
      operationId: list_templates_api_v1_channels__channel_id__templates_get
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: sync
        in: query
        required: false
        schema:
          type: boolean
          description: Sync templates from Meta before returning. Set false for faster
            cached data (may be stale).
          default: true
          title: Sync
        description: Sync templates from Meta before returning. Set false for faster
          cached data (may be stale).
      - name: status
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Filter by status: APPROVED, PENDING, or REJECTED.'
          title: Status
        description: 'Filter by status: APPROVED, PENDING, or REJECTED.'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateListResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    post:
      tags:
      - api
      summary: Create Template
      description: 'Create a new WhatsApp message template.


        The template is submitted to Meta for review (status=PENDING).

        Review typically takes up to 24 hours.


        Template name must be lowercase alphanumeric with underscores.

        BODY component is required. HEADER (TEXT only), FOOTER, and BUTTONS are optional.


        Variables use {{1}}, {{2}} syntax (positional) or {{name}} (named).

        Example values are required for Meta review.


        Rate limit: 100 templates created per WABA per hour.'
      operationId: create_template_api_v1_channels__channel_id__templates_post
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateCreateRequest'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/{channel_id}/templates/{template_id}:
    get:
      tags:
      - api
      summary: Get Template
      description: 'Get a specific template by its ID.


        Returns cached template data including variable definitions

        with example values for each variable.'
      operationId: get_template_api_v1_channels__channel_id__templates__template_id__get
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: template_id
        in: path
        required: true
        schema:
          type: string
          title: Template Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    put:
      tags:
      - api
      summary: Update Template
      description: 'Update a template''s components.


        All components are replaced entirely — partial update is not supported by
        Meta.


        Restrictions:

        - Only APPROVED, REJECTED, or PAUSED templates can be edited

        - Approved templates: max 10 edits per 30 days, 1 per 24 hours

        - Cannot change category of an approved template


        After editing an approved template, it goes back to PENDING for re-review.'
      operationId: update_template_api_v1_channels__channel_id__templates__template_id__put
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: template_id
        in: path
        required: true
        schema:
          type: string
          title: Template Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateUpdateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api
      summary: Delete Template
      description: 'Delete a message template.


        By default, deletes only the specific language version.

        Set all_languages=true to delete ALL language versions of this template.


        WARNING: After deleting an approved template, you cannot create a template

        with the same name for 30 days.'
      operationId: delete_template_api_v1_channels__channel_id__templates__template_id__delete
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: template_id
        in: path
        required: true
        schema:
          type: string
          title: Template Id
      - name: all_languages
        in: query
        required: false
        schema:
          type: boolean
          description: Delete all language versions of this template, not just this
            one.
          default: false
          title: All Languages
        description: Delete all language versions of this template, not just this
          one.
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/messages/send-template:
    post:
      tags:
      - api
      summary: Send Template Message
      description: 'Send an approved WhatsApp template without automatic retry.


        Variables must match the cached template definition. Positional variables

        use arrays such as `{"body": ["Pablo", "ORD-123"]}`; named variables use

        objects such as `{"body": {"customer_name": "Pablo"}}`. `recipient` follows

        the same preflight as the text endpoint: trimmed, then `400 invalid_recipient`

        / `recipient_equals_sender` before any Meta call.'
      operationId: send_template_message_api_v1_messages_send_template_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendTemplateRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
components:
  schemas:
    AvailableSlotsByTier:
      properties:
        total:
          type: integer
          title: Total
          description: Total free active slots for this channel type.
        starter:
          type: integer
          title: Starter
          description: Free Starter subscription slots for this channel type.
        pro:
          type: integer
          title: Pro
          description: Free Pro subscription slots for this channel type.
      type: object
      required:
      - total
      - starter
      - pro
      title: AvailableSlotsByTier
      description: Free active subscription slots for one channel type.
    ChannelHealth:
      properties:
        status:
          type: string
          enum:
          - ok
          - action_required
          title: Status
          description: '''ok'', or ''action_required'' when Meta no longer lets Fiwano
            work with this account or number (access or a permission revoked, number
            removed or not connected to the WhatsApp Business Platform, account blocked
            by Meta). The channel is not deactivated, but messages may fail until
            the owner fixes the cause — in Meta, or by running the connect flow again
            for the same account. Clears automatically once Meta''s checks pass, and
            on reconnect.'
        reason:
          anyOf:
          - type: string
          - type: 'null'
          title: Reason
          description: 'Human-readable text, usually Meta''s own error as ''(#<code>)
            <text>''. Not a code: branch on `status`. Null when status is ''ok''.'
        since:
          anyOf:
          - type: string
          - type: 'null'
          title: Since
          description: ISO-8601 UTC timestamp when the problem was detected. Null
            when status is 'ok'.
      type: object
      required:
      - status
      title: ChannelHealth
      description: 'Meta-side state of a channel: whether Meta still lets Fiwano work
        with the

        account and number. Failures of individual messages are not reflected here
        —

        they come in the send response and the `message.failed` webhook.'
    ChannelListResponse:
      properties:
        channels:
          items:
            $ref: '#/components/schemas/ChannelOut'
          type: array
          title: Channels
        total:
          type: integer
          title: Total
      type: object
      required:
      - channels
      - total
      title: ChannelListResponse
      description: List of channels.
    ChannelOut:
      properties:
        id:
          type: string
          title: Id
          description: Channel ID. Use this value in every other channel/message API
            call.
        channel_type:
          type: string
          title: Channel Type
          description: 'Channel type: ''whatsapp'', ''instagram'', or ''facebook''.'
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
          description: 'Display name: business name (WhatsApp), username (Instagram),
            or Page name (Facebook).'
        is_active:
          type: boolean
          title: Is Active
          description: False only after the channel was deactivated (DELETE, or its
            subscription ended). An active channel can still have a Meta-side problem
            — see `health`.
        health:
          $ref: '#/components/schemas/ChannelHealth'
          description: Meta-side state of the channel.
        phone_number_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone Number Id
          description: WhatsApp only — Meta's phone number ID.
        phone_number:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone Number
          description: WhatsApp only — human-readable phone number.
        waba_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Waba Id
          description: WhatsApp only — WhatsApp Business Account (WABA) ID.
        quality_rating:
          anyOf:
          - type: string
          - type: 'null'
          title: Quality Rating
          description: WhatsApp only — Meta's current quality rating for the number,
            when available.
        ig_account_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Ig Account Id
          description: Instagram only — Instagram account ID.
        ig_username:
          anyOf:
          - type: string
          - type: 'null'
          title: Ig Username
          description: Instagram only — Instagram username.
        page_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Page Id
          description: Instagram/Facebook — linked Facebook Page ID.
        webhook_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Url
          description: Where incoming messages and delivery statuses are delivered.
        has_webhook_secret:
          type: boolean
          title: Has Webhook Secret
          description: Whether a webhook secret is configured for HMAC signature verification.
          default: false
        webhook_events:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Webhook Events
          description: Enabled event types, e.g. ["message.received", "message.delivered"].
        echo_statuses:
          type: boolean
          title: Echo Statuses
          description: 'message.echo status mode: true = tracked (echoed external
            messages get a durable identity and their delivered/read/failed statuses
            are delivered per your webhook_events), false = relay-only (echo events
            without status tracking; the default). Has no effect until "message.echo"
            is enabled in webhook_events.'
          default: false
        connected_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Connected At
          description: ISO-8601 UTC timestamp when the channel was connected.
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Created At
          description: ISO-8601 UTC timestamp when the channel record was first created.
        subscription:
          $ref: '#/components/schemas/SubscriptionInfo'
          description: Current subscription/billing state. The channel can send/receive
            only while status='active'.
      type: object
      required:
      - id
      - channel_type
      - is_active
      - health
      - subscription
      title: ChannelOut
      description: 'A connected channel — one WhatsApp number, Instagram account,
        or Facebook

        Page. Channel-type-specific fields are populated only for the relevant type

        (e.g. `phone_number_id`/`waba_id` for WhatsApp, `ig_username` for Instagram);

        the rest are null.'
    ChannelUpdateRequest:
      properties:
        webhook_url:
          anyOf:
          - type: string
            maxLength: 500
          - type: 'null'
          title: Webhook Url
          description: HTTPS URL for incoming webhook delivery. Explicit ports are
            supported.
        webhook_secret:
          anyOf:
          - type: string
            maxLength: 64
          - type: 'null'
          title: Webhook Secret
          description: Set a specific HMAC secret (max 64 characters; also use this
            to rotate to a new value). Omit to keep the current secret — except that
            setting webhook_url for the first time with no secret auto-generates one,
            returned in the response.
        webhook_events:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Webhook Events
          description: List of event types to deliver. Available events depend on
            channel type.
        echo_statuses:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Echo Statuses
          description: 'message.echo status mode: true = tracked (echoed external
            messages get a durable identity and their delivered/read/failed statuses
            are delivered per your webhook_events), false = relay-only (echo events
            without status tracking; the default). Has no effect until "message.echo"
            is enabled in webhook_events.'
        subscription_id:
          anyOf:
          - type: string
            maxLength: 16
          - type: 'null'
          title: Subscription Id
          description: 'Move the channel to another subscription, or release its subscription
            slot. Pass a subscription ID from GET /api/v1/subscriptions to bind the
            channel to it — allowed for an active or a deactivated channel, and the
            target subscription must be active with a free slot for this channel type.
            Pass an empty string ("") to unbind: this frees the slot so a different
            channel of the same type can be connected, and is allowed only for a channel
            already deactivated via DELETE /api/v1/channels/{channel_id}. Omit the
            field (or send null) to leave the current binding untouched. Note that
            moving a channel from a Pro to a Starter subscription immediately removes
            access to media and template sending, and that unbinding is effectively
            permanent: reconnecting that channel later requires a free slot again.'
      type: object
      title: ChannelUpdateRequest
      description: Request to update channel settings.
    ChannelUpdateResponse:
      properties:
        id:
          type: string
          title: Id
        webhook_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Url
        webhook_secret:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Secret
        webhook_events:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Webhook Events
        echo_statuses:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Echo Statuses
        subscription:
          anyOf:
          - $ref: '#/components/schemas/SubscriptionInfo'
          - type: 'null'
          description: Subscription state of the channel after the update. Same shape
            as `subscription` on GET /api/v1/channels/{channel_id}.
      type: object
      required:
      - id
      title: ChannelUpdateResponse
      description: Response after channel update.
    ExchangeCodeRequest:
      properties:
        code:
          type: string
          title: Code
          description: One-time completion code from OAuth redirect
        webhook_url:
          anyOf:
          - type: string
            maxLength: 500
          - type: 'null'
          title: Webhook Url
          description: HTTPS URL for incoming webhook delivery. Explicit ports are
            supported. If provided, webhook is configured automatically — no separate
            PATCH needed.
        webhook_secret:
          anyOf:
          - type: string
            maxLength: 64
          - type: 'null'
          title: Webhook Secret
          description: 'HMAC secret used to sign webhook deliveries (verify it via
            the X-Webhook-Signature header). Optional: if you set webhook_url without
            supplying this, Fiwano auto-generates a secret and returns it in the response.
            Pass your own value (max 64 characters) to use a specific secret instead.'
        webhook_events:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Webhook Events
          description: List of event types to deliver. Available events depend on
            channel type. If omitted, no events are delivered until configured.
        echo_statuses:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Echo Statuses
          description: 'message.echo status mode: true = tracked (echoed external
            messages get a durable identity and their delivered/read/failed statuses
            are delivered per your webhook_events), false = relay-only (echo events
            without status tracking; the default). Has no effect until "message.echo"
            is enabled in webhook_events.'
      type: object
      required:
      - code
      title: ExchangeCodeRequest
      description: Request to exchange completion code for channel data.
    ExchangeCodeResponse:
      properties:
        channel_id:
          type: string
          title: Channel Id
        channel_type:
          type: string
          title: Channel Type
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
        phone_number_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone Number Id
        phone_number:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone Number
        ig_account_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Ig Account Id
        ig_username:
          anyOf:
          - type: string
          - type: 'null'
          title: Ig Username
        page_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Page Id
        webhook_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Url
        webhook_secret:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Secret
        webhook_events:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Webhook Events
        echo_statuses:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Echo Statuses
      type: object
      required:
      - channel_id
      - channel_type
      title: ExchangeCodeResponse
      description: Response with channel data after code exchange.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    RedirectCreateRequest:
      properties:
        uri_pattern:
          type: string
          maxLength: 500
          title: Uri Pattern
          description: HTTPS redirect URI. Explicit ports are supported. Prefer an
            exact URI; wildcards are limited to paths, queries, or a complete left-most
            hostname label such as https://*.example.com/*
      type: object
      required:
      - uri_pattern
      title: RedirectCreateRequest
      description: Request to add redirect URI.
    RedirectCreateResponse:
      properties:
        id:
          type: string
          title: Id
        uri_pattern:
          type: string
          title: Uri Pattern
      type: object
      required:
      - id
      - uri_pattern
      title: RedirectCreateResponse
      description: Response after creating redirect.
    RedirectListResponse:
      properties:
        redirects:
          items:
            $ref: '#/components/schemas/RedirectOut'
          type: array
          title: Redirects
      type: object
      required:
      - redirects
      title: RedirectListResponse
      description: List of redirect URIs.
    RedirectOut:
      properties:
        id:
          type: string
          title: Id
        uri_pattern:
          type: string
          title: Uri Pattern
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Created At
      type: object
      required:
      - id
      - uri_pattern
      title: RedirectOut
      description: Allowed redirect URI.
    SendMediaRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Channel ID to send from
        recipient:
          type: string
          maxLength: 100
          title: Recipient
          description: Recipient identifier
        media_type:
          type: string
          title: Media Type
          description: 'Media type: image, audio, video, document, or sticker'
        media_url:
          anyOf:
          - type: string
            maxLength: 2048
          - type: 'null'
          title: Media Url
          description: HTTPS URL of the media file. For non-public content, use a
            signed URL (S3 presigned, GCS Signed, R2 signed, or HMAC). Max length
            2048. Required for every media_type except a Facebook Messenger sticker.
        sticker_id:
          anyOf:
          - type: string
            maxLength: 32
          - type: 'null'
          title: Sticker Id
          description: Meta sticker id (digits) — Facebook Messenger only, with media_type
            'sticker' instead of media_url. A catalog sticker or 369239263222822 (thumbs
            up); reuse media.sticker_id from an inbound event.
        caption:
          anyOf:
          - type: string
            maxLength: 1024
          - type: 'null'
          title: Caption
          description: Caption (WhatsApp image/video/document)
        filename:
          anyOf:
          - type: string
            maxLength: 255
          - type: 'null'
          title: Filename
          description: Filename (WhatsApp document only)
      type: object
      required:
      - channel_id
      - recipient
      - media_type
      title: SendMediaRequest
      description: 'Request to send a media message. Requires a Pro license.


        Meta fetches the file directly from `media_url`. For non-public

        content, use a signed URL — presigned S3, GCS Signed URL, Cloudflare

        R2 signed URL, or HMAC-signed URL on your own server. Public URLs

        are accessible to anyone who learns them; only use them for

        non-sensitive content.


        Use image, audio, video, or document across all supported channels.

        Provider-specific attachment names are handled internally.


        `sticker` is channel-specific: WhatsApp takes a WebP `media_url`,

        Facebook Messenger takes a Meta catalog `sticker_id` instead of a URL,

        Instagram has no sticker message. Which field is required is decided

        against the channel in the send preflight (`400 invalid_media_request`);

        the schema only checks that exactly one of them is given.'
    SendMessageRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Channel ID to send from
        recipient:
          type: string
          maxLength: 100
          title: Recipient
          description: Recipient identifier (phone number for WhatsApp, IGSID for
            Instagram, PSID for Facebook)
        text:
          type: string
          minLength: 1
          title: Text
          description: Message text containing at least one non-whitespace character
      type: object
      required:
      - channel_id
      - recipient
      - text
      title: SendMessageRequest
      description: Request to send a message. Only text messages are supported.
    SendMessageResponse:
      properties:
        success:
          type: boolean
          title: Success
        message_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Message Id
        error:
          anyOf:
          - type: string
          - type: 'null'
          title: Error
          description: 'Readable reason for a failed send: Meta''s text, plus a Fiwano
            hint where Meta''s text does not explain the problem.'
        error_code:
          anyOf:
          - type: integer
          - type: 'null'
          title: Error Code
          description: Meta's error code, unchanged, when Meta rejected the send (https://fiwano.com/documentation/errors#send-error-codes).
            Null for 'sent'/'queued' and for a failure without a Meta error (network,
            Meta 5xx without a body).
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
          description: 'Delivery state: ''sent'' (accepted by Meta), ''queued'' (Fiwano
            is completing the send in the background after a temporary Meta failure
            on text/media or a slow Meta response on any send; the outcome follows
            as webhooks), or ''failed'' (not sent and not retried; see error/error_code).'
      type: object
      required:
      - success
      title: SendMessageResponse
      description: Response after sending a message.
    SendTemplateRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: WhatsApp channel ID to send from
        template_name:
          type: string
          title: Template Name
          description: Template name (e.g., 'order_confirmation')
        language:
          type: string
          title: Language
          description: Template language code (e.g., 'en_US')
        recipient:
          type: string
          maxLength: 100
          title: Recipient
          description: Recipient phone number (without +, e.g., '1234567890')
        variables:
          anyOf:
          - type: object
          - type: 'null'
          title: Variables
          description: 'Variable values keyed by component type. Positional: {"header":
            ["Sale"], "body": ["Pablo", "ORD-123"], "buttons": [{"index": 0, "value":
            "promo"}]}. Named: {"body": {"customer_name": "Pablo", "order_number":
            "ORD-123"}}. Omit if template has no variables.'
      type: object
      required:
      - channel_id
      - template_name
      - language
      - recipient
      title: SendTemplateRequest
      description: "Request to send a WhatsApp template message.\n\nVariables must\
        \ match the template's parameter definitions.\nOnly APPROVED templates can\
        \ be sent.\n\nFor positional templates, provide variables as arrays:\n   \
        \ {\"body\": [\"Pablo\", \"ORD-123\"]}\n\nFor named templates, provide variables\
        \ as objects:\n    {\"body\": {\"customer_name\": \"Pablo\", \"order_number\"\
        : \"ORD-123\"}}"
    SenderProfileResponse:
      properties:
        channel_id:
          type: string
          title: Channel Id
        channel_type:
          type: string
          title: Channel Type
        user_id:
          type: string
          title: User Id
        profile:
          anyOf:
          - type: object
          - type: 'null'
          title: Profile
          description: 'Profile data from Meta, normalized and minimized by channel
            type. null if profile is unavailable. Instagram: username, name, profile_pic,
            follower_count, is_verified_user. Facebook: display name in first_name;
            last_name and profile_pic when available from Meta. WhatsApp is not supported.'
          examples:
          - follower_count: 46
            is_verified_user: false
            name: Roman Babakin
            profile_pic: https://scontent.cdninstagram.com/v/t51.2885-19/...
            username: winnerzzz
          - first_name: Roman
            last_name: Babakin
            profile_pic: https://platform-lookaside.fbsbx.com/platform/profilepic/...
        cached:
          type: boolean
          title: Cached
          description: Whether the result was served from cache
          default: false
      type: object
      required:
      - channel_id
      - channel_type
      - user_id
      title: SenderProfileResponse
      description: Sender profile from Meta Graph API.
    SetupUrlRequest:
      properties:
        channel_type:
          type: string
          pattern: ^(whatsapp|instagram|facebook)$
          title: Channel Type
          description: 'Channel type: whatsapp, instagram, or facebook'
        redirect_uri:
          type: string
          maxLength: 500
          title: Redirect Uri
          description: Exact HTTPS URL to redirect after OAuth completion. Explicit
            ports are supported. Must match allowed_redirects.
      type: object
      required:
      - channel_type
      - redirect_uri
      title: SetupUrlRequest
      description: Request to generate channel setup URL.
    SetupUrlResponse:
      properties:
        setup_url:
          type: string
          title: Setup Url
          description: URL to open in popup/browser for channel setup
        session_id:
          type: string
          title: Session Id
        expires_at:
          type: string
          title: Expires At
      type: object
      required:
      - setup_url
      - session_id
      - expires_at
      title: SetupUrlResponse
      description: Response with channel setup URL.
    SubscriptionAssignedChannel:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Fiwano channel ID assigned to this subscription.
        channel_type:
          type: string
          title: Channel Type
          description: 'Channel type: whatsapp, instagram, or facebook.'
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
          description: Human-readable channel name when available.
        is_active:
          type: boolean
          title: Is Active
          description: Whether the bound channel is currently active.
      type: object
      required:
      - channel_id
      - channel_type
      - is_active
      title: SubscriptionAssignedChannel
      description: 'Channel currently assigned to this subscription for one channel
        type.


        An inactive channel still occupies its slot while it remains bound to an

        active subscription. This preserves the owner''s reconnect path and matches

        the billing gate used by channel setup.'
    SubscriptionInfo:
      properties:
        id:
          anyOf:
          - type: string
          - type: 'null'
          title: Id
          description: Subscription ID this channel is bound to — the same value as
            `subscriptions[].id` in GET /api/v1/subscriptions. Pass it back in PATCH
            /api/v1/channels/{channel_id} to move the channel between subscriptions.
            Null when status='none'.
        status:
          type: string
          title: Status
          description: 'Subscription status: ''active'', ''expired'', ''canceled'',
            or ''none'' (no license bound — channel cannot send/receive messages).'
        source:
          anyOf:
          - type: string
          - type: 'null'
          title: Source
          description: 'Origin of the license: ''trial'' (auto-created on signup),
            ''paddle'' (paid subscription), or ''enterprise'' (admin-granted custom
            subscription). Null when status=''none''.'
        tier:
          anyOf:
          - type: string
          - type: 'null'
          title: Tier
          description: 'License tier: ''starter'' or ''pro''. Null when status=''none''.'
        expires_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Expires At
          description: ISO-8601 UTC timestamp when the current billing period / license
            ends. Null when status='none' or for perpetual enterprise licenses.
        auto_renew:
          type: boolean
          title: Auto Renew
          description: True only for an active Paddle subscription that is set to
            renew automatically at `expires_at`. False for trial, enterprise, or any
            Paddle subscription where the user has scheduled/performed a cancellation.
          default: false
      type: object
      required:
      - status
      title: SubscriptionInfo
      description: 'Subscription/license state for a channel.


        Always present on ChannelOut. When the channel is not bound to any license

        (orphaned after expiry/cancellation, or briefly between connect and

        auto-assign), `status` is `"none"` and all other fields are null/false.


        `expires_at` is `null` when no license is bound; in normal operation

        every active license (trial / Paddle / Enterprise) carries an explicit

        expiration date. (The schema still permits `NULL` for legacy admin-

        granted rows — clients should treat that as "no announced end date".)


        `auto_renew` is `true` only for an active Paddle subscription with no

        scheduled cancellation. Once the user cancels in Paddle (or the

        subscription enters a non-renewing state) it flips to `false` and the

        license will lapse at `expires_at` unless resumed.'
    SubscriptionOut:
      properties:
        id:
          type: string
          title: Id
          description: Fiwano subscription ID.
        status:
          type: string
          title: Status
          description: 'Subscription status: active, expired, or canceled.'
        source:
          type: string
          title: Source
          description: 'Origin: trial, paddle, or enterprise.'
        tier:
          type: string
          title: Tier
          description: 'Subscription tier: starter or pro.'
        starts_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Starts At
          description: ISO-8601 UTC timestamp when the subscription started.
        expires_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Expires At
          description: ISO-8601 UTC timestamp when the current period ends. Treat
            status as the source of truth for operability; Paddle grace can leave
            an active subscription with expires_at in the past.
        auto_renew:
          type: boolean
          title: Auto Renew
          description: True only for an active Paddle subscription set to renew automatically.
          default: false
        assigned_channels:
          additionalProperties:
            anyOf:
            - $ref: '#/components/schemas/SubscriptionAssignedChannel'
            - type: 'null'
          type: object
          title: Assigned Channels
          description: Channels assigned to this subscription by channel type. Values
            are null when no channel is assigned for that type. Use top-level available_slots
            for current connection availability.
      type: object
      required:
      - id
      - status
      - source
      - tier
      - assigned_channels
      title: SubscriptionOut
      description: 'A Fiwano subscription/license and its channel-slot availability.


        Public API uses the product term "subscription"; internally this maps to a

        License row. One subscription grants one slot for each channel type.'
    SubscriptionsResponse:
      properties:
        subscriptions:
          items:
            $ref: '#/components/schemas/SubscriptionOut'
          type: array
          title: Subscriptions
        total:
          type: integer
          title: Total
          description: Total number of subscriptions returned.
        active_total:
          type: integer
          title: Active Total
          description: Number of currently active subscriptions.
        available_slots:
          additionalProperties:
            $ref: '#/components/schemas/AvailableSlotsByTier'
          type: object
          title: Available Slots
          description: Free active subscription slots by channel type, split by tier.
            `total` is the sum of Starter and Pro free slots.
      type: object
      required:
      - subscriptions
      - total
      - active_total
      - available_slots
      title: SubscriptionsResponse
      description: Subscriptions and aggregate slot availability for the authenticated
        user.
    TemplateComponentInput:
      properties:
        type:
          type: string
          title: Type
          description: 'Component type: HEADER, BODY, FOOTER, BUTTONS'
        format:
          anyOf:
          - type: string
          - type: 'null'
          title: Format
          description: 'Header format: TEXT (media not yet supported)'
        text:
          anyOf:
          - type: string
          - type: 'null'
          title: Text
          description: Component text. Use {{1}}, {{2}} for positional or {{name}}
            for named variables
        example:
          anyOf:
          - type: object
          - type: 'null'
          title: Example
          description: Example values for variables (required by Meta for review)
        buttons:
          anyOf:
          - items:
              type: object
            type: array
          - type: 'null'
          title: Buttons
          description: Button definitions (for BUTTONS component)
      type: object
      required:
      - type
      title: TemplateComponentInput
      description: 'A single template component for creation/update.


        Components define the structure of a WhatsApp message template.'
    TemplateCreateRequest:
      properties:
        name:
          type: string
          maxLength: 512
          pattern: ^[a-z0-9_]+$
          title: Name
          description: Template name. Lowercase alphanumeric and underscores only.
            Max 512 chars.
        category:
          type: string
          pattern: ^(MARKETING|UTILITY|AUTHENTICATION)$
          title: Category
          description: 'Template category: MARKETING, UTILITY, or AUTHENTICATION'
        language:
          type: string
          title: Language
          description: Language code (e.g., en_US, ru, es)
        components:
          items:
            $ref: '#/components/schemas/TemplateComponentInput'
          type: array
          title: Components
          description: Template components (HEADER, BODY, FOOTER, BUTTONS). BODY is
            required.
        parameter_format:
          type: string
          pattern: ^(positional|named)$
          title: Parameter Format
          description: 'Variable format: ''positional'' for {{1}}, {{2}} or ''named''
            for {{customer_name}}'
          default: positional
      type: object
      required:
      - name
      - category
      - language
      - components
      title: TemplateCreateRequest
      description: 'Request to create a WhatsApp message template.


        The template will be submitted to Meta for review (status=PENDING).

        Review typically takes up to 24 hours.'
    TemplateListResponse:
      properties:
        templates:
          items:
            $ref: '#/components/schemas/TemplateOut'
          type: array
          title: Templates
        total:
          type: integer
          title: Total
        synced:
          type: boolean
          title: Synced
          default: false
      type: object
      required:
      - templates
      - total
      title: TemplateListResponse
      description: List of templates.
    TemplateOut:
      properties:
        id:
          type: string
          title: Id
        meta_template_id:
          type: string
          title: Meta Template Id
        name:
          type: string
          title: Name
        language:
          type: string
          title: Language
        category:
          type: string
          title: Category
        status:
          type: string
          title: Status
        components:
          items: {}
          type: array
          title: Components
        parameter_format:
          type: string
          title: Parameter Format
          default: positional
        variables:
          anyOf:
          - $ref: '#/components/schemas/TemplateVariablesSummary'
          - type: 'null'
        synced_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Synced At
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Created At
      type: object
      required:
      - id
      - meta_template_id
      - name
      - language
      - category
      - status
      - components
      title: TemplateOut
      description: Template data returned by API.
    TemplateUpdateRequest:
      properties:
        components:
          items:
            $ref: '#/components/schemas/TemplateComponentInput'
          type: array
          title: Components
          description: New components (replaces all existing)
        category:
          anyOf:
          - type: string
          - type: 'null'
          title: Category
          description: New category (only for REJECTED or PAUSED templates)
      type: object
      required:
      - components
      title: TemplateUpdateRequest
      description: 'Request to update a template''s components.


        All components are replaced entirely (partial update not supported by Meta).

        Approved templates: max 10 edits per 30 days, 1 per 24 hours.'
    TemplateVariableInfo:
      properties:
        position:
          type: integer
          title: Position
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
        example:
          anyOf:
          - type: string
          - type: 'null'
          title: Example
      type: object
      required:
      - position
      title: TemplateVariableInfo
      description: Variable info for display/documentation.
    TemplateVariablesSummary:
      properties:
        total_count:
          type: integer
          title: Total Count
          default: 0
        parameter_format:
          type: string
          title: Parameter Format
          default: positional
        header:
          anyOf:
          - items:
              $ref: '#/components/schemas/TemplateVariableInfo'
            type: array
          - type: 'null'
          title: Header
        body:
          anyOf:
          - items:
              $ref: '#/components/schemas/TemplateVariableInfo'
            type: array
          - type: 'null'
          title: Body
        buttons:
          anyOf:
          - items:
              type: object
            type: array
          - type: 'null'
          title: Buttons
      type: object
      title: TemplateVariablesSummary
      description: Summary of all variables in a template.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      description: API key in the format mip_live_xxx. Create one on the API Keys
        page in the portal.
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      description: 'The same API key sent as `Authorization: Bearer mip_live_xxx`.
        Used only when `X-API-Key` is absent.'
      scheme: bearer
```
