# 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 e o header X-API-Key.
2. **Erros** — Formato de erro e códigos de status HTTP.
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 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, verifique assinaturas HMAC, processe mídia e acompanhe 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. **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.
10. **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.
11. **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`.

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.

---

## Erros

Toda resposta de erro tem um campo `detail`. A maioria dos erros de domínio usa uma descrição legível:

```json
{ "detail": "Human-readable error description" }
```

A validação de esquema (`422`) usa uma lista de erros de campo. Alguns erros de
validação de domínio usam um objeto `detail` estruturado (por exemplo `text_too_long`
ao enviar um texto longo demais); os formatos por endpoint estão na
[Referência da API](/documentation/api).

### Códigos de status HTTP

| Código | Significado | O que fazer |
|---|---|---|
| `200` | Sucesso | — |
| `201` | Criado | — |
| `400` | Requisição inválida | Verifique o campo `detail` |
| `401` | Não autorizado | Verifique seu header `X-API-Key` |
| `402` | Pagamento necessário | Período de teste encerrado ou assinatura inativa — veja [Assinaturas e Cobrança](/br/documentation/subscriptions) |
| `404` | Não encontrado | O recurso não existe ou pertence a outra conta |
| `422` | Erro de validação | Verifique campos obrigatórios, tipos e restrições de campo em `detail` |
| `429` | Limite de taxa excedido | Reduza o ritmo e tente novamente após `Retry-After` — veja [limites de taxa](/br/documentation/capabilities#rate-limits) |
| `502` | Erro da API da Meta | Falha no upstream. Verifique `detail`. Tentar novamente pode ajudar. |
| `503` | Sobrecarregado temporariamente | Descarte de carga transitório. Tente novamente após `Retry-After`. |

---

## 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` — 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. 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 uma mensagem aceita com um `message_id`; 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.

Para o schema exato de request/response de cada endpoint de canal (campos, tipos,
códigos de status), veja a **[Referência da API](/documentation/api)**. Esta página é o
guia em nível de tarefa; ela não repete as tabelas de campos.

### Pré-requisitos

Antes de conectar qualquer canal — WhatsApp, Instagram ou Facebook Messenger — garanta
que ambas as condições abaixo sejam atendidas. Elas valem igualmente para o fluxo do
Portal e o fluxo da API; se alguma 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.

### 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.

Definir uma webhook URL no Portal **não** cria um `webhook_secret`. Defina um
explicitamente para que as entregas recebidas sejam assinadas — veja
[Segredo do webhook](#webhook-secret).
A 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 a identidade da Meta já pertencer a um canal inativo seu, a Fiwano
reativa essa mesma linha e `exchange-code` retorna o `channel_id` existente.
Quando um asset realmente novo e um asset inativo estão disponíveis ao mesmo
tempo, o asset novo tem prioridade.

**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 o diálogo da Meta. `setup_failed` — o setup não pôde ser concluído (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.

Os eventos disponíveis dependem do tipo de canal — o WhatsApp expõe mais
(`message.sent`, `message.failed`) do que Instagram e Facebook. A lista completa com os
payloads está na página de **[Webhooks](/br/documentation/webhooks#event-types)**. Um evento
que você listar e que não seja válido para o tipo de canal é simplesmente ignorado, não é
um erro.

**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**. Uma resposta não-2xx ou um timeout conta como entrega
falha: a Fiwano **tenta novamente com backoff e envia e-mail para você** — um aviso após a
3ª tentativa falha e um alerta quando as tentativas se esgotam. 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). Payloads de webhook entregues com sucesso não são retidos
   para repasse; falhas são armazenadas de forma criptografada para novas tentativas. Comportamento completo:
**[Webhooks → Política de novas tentativas](/br/documentation/webhooks#retry-policy)**.

### 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
**[Webhooks](/br/documentation/webhooks)** 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** (mínimo de 16 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` 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 inativo **mantém** o segredo existente (veja
  [Reconectando um canal inativo](#reconectando-um-canal-inativo) 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 exemplo de Webhooks).

### 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 | `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 `contact@fiwano.com`.

O Fiwano também cancela a inscrição do recurso de webhook da
Meta do canal apenas quando é seguro: uma inscrição de WABA é mantida se outro canal de
WhatsApp ativo usar a mesma WABA, e uma inscrição de Página é mantida se outro canal de
Instagram/Facebook ativo usar a mesma Página.

### Reconectando um canal inativo

Um canal fica inativo quando é desativado (`DELETE /api/v1/channels/{id}`) ou
quando sua conexão com a Meta não pode mais ser mantida (por exemplo, o dono da conta
revogou o acesso na Meta). Para trazê-lo de volta, execute 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):

- O canal existente é **reativado 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 **licença ativa**: o canal ainda deve ter uma, ou
  você deve ter um slot de licença livre. Caso contrário, o fluxo é recusado — vincule uma
  licença em Billing primeiro.
- 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
  `contact@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. O valor `text` deve conter pelo menos um
caractere que não seja espaço em branco; valores vazios ou somente com espaços são
rejeitados com `422` antes de chamar a Meta. Espaços no início e no fim de um texto válido
são preservados. Veja [Capacidades](/br/documentation/capabilities).

### Mensagens de mídia

`POST /api/v1/messages/send-media` — **licença Pro obrigatória.** A Meta busca o arquivo
diretamente de `media_url`; o Fiwano nunca o baixa nem o armazena. Passe `media_type`
(`image`, `audio`, `video`, `document`) 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.

```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. Falhas recuperáveis, incluindo
`131053` de processamento/fetcher e `131056` de limite do par remetente-destinatário,
retornam `queued` e usam o mesmo cronograma durável de novas tentativas que texto.
Os limites de tamanho de arquivo e a tabela completa de error codes estão em
[Capacidades](/br/documentation/capabilities) e na [Referência da API](/documentation/api).

### 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 retornam `200` com os mesmos campos `success`,
`message_id`, `error`, `error_code` e `status`, porque o que acontece depois que
a Meta aceita a requisição importa:

- **`sent`** — a Meta aceitou. Acompanhe o resto pelos webhooks de status de entrega
  (`message.delivered` / `read` / `failed`) — veja
  [Recebendo Mensagens](/br/documentation/webhooks#delivery-status-tracking).
- **`queued`** — uma falha transitória da Meta (rede, 5xx, rate limit). O Fiwano
  tenta novamente em segundo plano (até 7 vezes ao longo de ~20 min). Você recebe um e-mail
  de aviso antecipado após 3 tentativas falhas e um e-mail final se elas se esgotarem.
  Apenas `send` e `send-media` podem retornar `queued`.
- **`failed`** (`success: false`) — a requisição não será repetida. Para `send` e
  `send-media`, isso significa um erro permanente (destinatário inválido, texto
  acima do limite, payload malformado), e o dono do canal é notificado por e-mail.
  `send-template` não repete automaticamente, portanto qualquer erro de envio da
  Meta é retornado como `failed`; o cliente decide se e quando reenviar.

Portanto `200` por si só não significa "entregue" — sempre leia `success` e `status`.

---

## 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. A Fiwano recebe o webhook original da Meta vindo da
WhatsApp Cloud API, resolve o canal conectado, normaliza o payload, assina a
entrega se você configurou um `webhook_secret` e envia para você.

A diferença útil em relação a integrar direto na Meta é que 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.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` | A entrega da mensagem falhou | WhatsApp |

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


### 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` (UUID). Todos os webhooks de status subsequentes
referenciam esse mesmo UUID; o ID da Meta permanece interno ao Fiwano.

- `message_id` está sempre presente em todos os eventos de status — é um UUID gerado pelo Fiwano, não um ID interno da Meta.
- 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).
- Todos os canais usam exatamente o mesmo formato de webhook.
- **Cascata de leitura:** quando um usuário lê uma conversa, o Fiwano envia um webhook `message.read` separado para *cada* mensagem não lida — não apenas a mais recente.

### 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 e vem sempre de um conjunto fixo: `text`, `image`, `audio`, `video`, `document`, `sticker` (somente WhatsApp) ou `unsupported`. Faça o roteamento por ele. Os quatro tipos de mídia são exatamente os valores aceitos como `media_type` de saída, então um evento de mídia recebido pode ser encaminhado sem tabela de conversão — exceto `sticker`, que existe apenas na entrada e precisa ser recodificado para sair como `image`.

Instagram e Facebook Messenger também usam anexos para coisas que não são arquivo, como um post compartilhado ou um pin de localização. Esses chegam 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. Todos os arquivos da mensagem de origem são preparados antes da entrega do primeiro evento, e depois 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"`.

Apenas para WhatsApp recebido, a Meta fornece um indicador confiável de mensagem de voz. O Fiwano o expõe como `media.voice: true` quando presente. Instagram e Facebook Messenger não expõem um indicador de voz confiável equivalente pelo payload do webhook, então `media.voice` é omitido para esses canais.

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. |
| `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 — 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` e outros tipos de mensagem que não são mídia |
| Instagram, Facebook Messenger | `share` e `ig_reel` (post ou reel compartilhado), `story_mention`, `location`, `fallback` (link compartilhado), `template`, `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 são ignoradas e não são entregues como eventos de webhook.

#### 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 (WhatsApp)

```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"}]
  }
}
```

### 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 são criptografados em repouso durante as novas tentativas e apagados após a entrega ou expiração

**Importante:** Seu endpoint **deve responder com HTTP 2xx em até 5 segundos**. Respostas não-2xx ou timeouts disparam a fila de novas tentativas. A Fiwano não retém para repasse os payloads de webhook entregues com sucesso; após uma falha na entrega inicial, o payload criptografado é armazenado temporariamente para novas tentativas automáticas.

> **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). Resultados bem-sucedidos são cacheados por 5 minutos; resultados indisponíveis são cacheados brevemente para que uma conversa recém-indexada possa ser consultada novamente em pouco tempo. 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)
```

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).

---

## Capacidades e Limites

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

### Capacidades por canal

Os três canais são conectados da mesma forma (OAuth). A tabela abaixo mostra o que cada canal suporta.

| 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 | image, audio, video, document | image, audio, video, document |
| 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, sticker | image, audio, video, document | image, audio, video, document |
| Status de entrega | `sent` `delivered` `read` `failed` | `delivered` `read` | `delivered` `read` |
| 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 um usuário 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`. As outras operações da API pública não compartilham um limite
global de RPS. 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:

- **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.
- **Instagram e Facebook Messenger** — você só pode responder dentro de **24 horas** da
  última mensagem do usuário. Não há contorno por modelo — aguarde o usuário enviar
  mensagem novamente.

### Limites de mídia

- **Mídia recebida** (imagens, áudio, vídeo, documentos) é armazenada temporariamente por
  **60 minutos**. Baixe-a via `GET /api/v1/media/{media_id}` prontamente após o webhook;
  os arquivos são removidos automaticamente após a expiração. 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).

---

## 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 licença seja (re)vinculada.

### 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 licença 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 o canal fica
  órfão.
- **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 licença 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 inativo ainda pode ocupar um slot porque
a Fiwano preserva o vínculo para reconexão. 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.

---

## 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 Webhook, Delete |
| 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 |

### Node Trigger — eventos

Inicia seu workflow para qualquer um destes eventos (filtre por tipo de evento nas configurações do node):

| Evento | Canais |
|---|---|
| `message.received` | WhatsApp, Instagram, Facebook |
| `message.delivered` | WhatsApp, Instagram, Facebook |
| `message.read` | WhatsApp, Instagram, Facebook |
| `message.sent` | WhatsApp |
| `message.failed` | WhatsApp |

### 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 Webhook** 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 Webhook**. 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`/`message.failed` 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 Webhook** 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 Webhook** 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). 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'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /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'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /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
      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.


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

        Optionally provide a custom webhook_secret for HMAC verification.'
      operationId: update_channel_api_v1_channels__channel_id__patch
      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
      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 inactive channel, setup reactivates that same

        channel row and returns its existing channel_id. A genuinely new eligible

        identity is preferred when both new and inactive assets are available.'
      operationId: create_setup_url_api_v1_channels_setup_url_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupUrlRequest'
      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'
  /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:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeCodeRequest'
      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'
  /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
      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'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    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:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectCreateRequest'
      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'
  /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
      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.'
      operationId: send_message_api_v1_messages_send_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
      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'
  /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
      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, and document. Transient

        failures are queued; permanent payload or policy errors are not retried.'
      operationId: send_media_message_api_v1_messages_send_media_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMediaRequest'
      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'
  /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
      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
      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
      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
      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
      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"}}`.'
      operationId: send_template_message_api_v1_messages_send_template_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendTemplateRequest'
      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'
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.
    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: True if the channel can currently send and receive messages.
        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"].
        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
      - 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
          - type: 'null'
          title: Webhook Secret
          description: Set a specific HMAC secret (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.
      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
      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
          - 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 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.
      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
      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
          title: Recipient
          description: Recipient identifier
        media_type:
          type: string
          title: Media Type
          description: 'Media type: image, audio, video, or document'
        media_url:
          type: string
          maxLength: 2048
          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.
        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
      - media_url
      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.'
    SendMessageRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Channel ID to send from
        recipient:
          type: string
          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
        error_code:
          anyOf:
          - type: integer
          - type: 'null'
          title: Error Code
          description: Meta error code (when failure originates at Meta). See documentation
            for common send error codes.
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
          description: 'Delivery state: ''sent'' (accepted by Meta), ''queued'' (transient
            text/media failure — retried in the background), or ''failed'' (terminal
            for this request; see error/error_code). Template sends are never queued.'
      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
          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:
        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
      in: header
      name: X-API-Key
      description: API key in the format mip_live_xxx. Create one on the API Keys
        page in the portal.
security:
- ApiKeyAuth: []
```
