{
  "openapi": "3.0.3",
  "info": {
    "title": "Digiwhats — User API",
    "version": "1.3.0",
    "description": "API REST por empresa do **Digiwhats** — conecte canais e opere mensagens, ações, templates e webhooks de saída, tudo com uma única API key.\n\n## Autenticação\n\nToda chamada exige a **API key da sua empresa** (prefixo `dwuser_live_`), enviada em **um** destes headers:\n\n- `Authorization: Bearer SUA_CHAVE`\n- `X-API-Key: SUA_CHAVE`\n\nO administrador gera a chave no painel, em **API Keys**.\n\n## Base URL\n\n`https://app.digiwhats.com.br/api/v1/company`\n\nO mesmo domínio pelo qual você acessa o painel (inclusive white label).\n\n## Canais suportados\n\n| Canal | Descrição |\n| --- | --- |\n| `whatsmeow`, `baileys` | WhatsApp **não-oficial** — conecta por QR ou código de pareamento |\n| `cloud` | WhatsApp **oficial** (Cloud API da Meta) — conecta por credenciais |\n| `instagram` | **Instagram Direct** |\n| `telegram` | **Telegram** |\n\nCada tipo suporta um conjunto diferente de recursos. Use o **seletor no topo da página** para filtrar a documentação por canal, ou consulte `GET /capabilities`.\n\n## Endereçamento\n\nO modo de endereçar depende de a mensagem ser **nova** ou **já existente**:\n\n- **Mensagem nova** — `POST /messages` e os sinais de presença (`POST /typing`, `POST /read`) usam o **telefone** no campo `to`. Hoje atendem **apenas canais de WhatsApp**.\n- **Ação sobre mensagem existente** — **reagir**, **editar**, **apagar** e **consultar status** usam o **`id`** da mensagem; o canal e o destino são derivados dela. Funcionam também para **Instagram** e **Telegram** (mensagens recebidas via webhook).\n\n## Recursos\n\n- **Mensagens** — texto, mídia, localização, contato, interativas e templates.\n- **Ações** — reação, editar, apagar, indicador de digitação e marcar como lida.\n- **Templates** — gestão dos templates HSM da API oficial (Cloud).\n- **Webhooks de saída** — a plataforma faz `POST` no seu servidor a cada mensagem recebida ou mudança de status."
  },
  "servers": [{ "url": "https://app.digiwhats.com.br/api/v1/company", "description": "Use o mesmo domínio pelo qual você acessa o painel." }],
  "security": [{ "bearerAuth": [] }, { "apiKeyHeader": [] }],
  "tags": [
    { "name": "Canais", "description": "Criar, conectar (QR/código), consultar e excluir canais." },
    { "name": "Mensagens", "description": "Enviar mensagens e consultar status de entrega." },
    { "name": "Ações", "description": "Ações sobre uma conversa/mensagem: reagir, editar, apagar, indicador de digitação e marcar como lida." },
    { "name": "Mídia", "description": "Upload de arquivos para envio." },
    { "name": "Templates", "description": "Ciclo de vida dos templates HSM da API oficial (Cloud): listar, sincronizar, submeter, editar, flags e excluir." },
    { "name": "Webhooks", "description": "Endpoints de webhook de SAÍDA: a plataforma faz POST no seu servidor quando chega uma mensagem (message.received) ou muda o status de uma enviada (message.status). O corpo é assinado com HMAC-SHA256 sobre `\"<timestamp>.<body>\"` usando o segredo do endpoint (devolvido só na criação); a assinatura vai no header `X-Digiwhats-Signature: sha256=<hex>`, com `X-Digiwhats-Timestamp`, `X-Digiwhats-Event` e `X-Digiwhats-Delivery` (id único — use para deduplicar). Responda 2xx para confirmar; falhas são reentregues com backoff exponencial." },
    { "name": "Utilidades", "description": "Recursos auxiliares (matriz de capacidades por provedor)." }
  ],
  "paths": {
    "/channels": {
      "post": {
        "tags": ["Canais"],
        "summary": "Criar canal (não-oficial)",
        "x-channels": ["whatsmeow", "baileys"],
        "description": "Cria um canal **não-oficial** (whatsmeow/baileys), que conecta por QR ou código de pareamento. Para a **API oficial (Cloud)**, use `POST /channels/cloud`. Requer `channels:write`.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateChannelRequest" }, "example": { "name": "API Vendas", "provider": "whatsmeow" } } }
        },
        "responses": {
          "201": { "description": "Canal criado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Channel" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "description": "Limite de canais atingido", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "tags": ["Canais"],
        "summary": "Listar canais",
        "x-channels": ["whatsmeow", "baileys", "cloud"],
        "description": "Lista os canais da empresa. Requer `channels:read`.",
        "responses": {
          "200": { "description": "Lista de canais", "content": { "application/json": { "schema": { "type": "object", "properties": { "channels": { "type": "array", "items": { "$ref": "#/components/schemas/Channel" } } } } } } }
        }
      }
    },
    "/channels/cloud": {
      "post": {
        "tags": ["Canais"],
        "summary": "Conectar canal Cloud (API oficial da Meta)",
        "x-channels": ["cloud"],
        "description": "Provisiona um canal da **API oficial (Cloud)** com as credenciais que a empresa já tem na Meta (Phone Number ID + access token permanente + WABA opcional) — **sem QR** e sem o popup do Embedded Signup. O token é validado na Graph API e o canal já fica conectado. Requer `channels:write`.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CloudChannelRequest" }, "example": { "name": "Vendas (Cloud)", "phoneNumberId": "120376389012345", "accessToken": "EAAG…", "wabaId": "1029384756" } } }
        },
        "responses": {
          "201": { "description": "Canal Cloud conectado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Channel" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "description": "Cloud API não habilitada na plataforma (CLOUD_NOT_CONFIGURED)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "A Graph API rejeitou as credenciais (GRAPH_API_ERROR)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/channels/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "get": {
        "tags": ["Canais"],
        "summary": "Consultar canal (status)",
        "x-channels": ["whatsmeow", "baileys", "cloud"],
        "description": "Status do canal (created/connecting/connected/disconnected). Requer `channels:read`.",
        "responses": {
          "200": { "description": "Canal", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Channel" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Canais"],
        "summary": "Excluir canal",
        "x-channels": ["whatsmeow", "baileys", "cloud"],
        "description": "Remove o canal. Requer `channels:write`.",
        "responses": { "200": { "$ref": "#/components/responses/Ok" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/channels/{id}/connect": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "post": {
        "tags": ["Canais"],
        "summary": "Conectar (gera QR)",
        "x-channels": ["whatsmeow", "baileys"],
        "description": "Inicia o pareamento por QR (só **whatsmeow/baileys**; a Cloud conecta em `POST /channels/cloud`). Depois consulte `GET /channels/{id}/qr` (polling) e `GET /channels/{id}` até `status=connected`. Requer `channels:write`.",
        "responses": {
          "202": { "description": "Conectando", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "connecting" } } } } } },
          "429": { "description": "Cooldown de conexão (aguarde)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/channels/{id}/qr": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "get": {
        "tags": ["Canais"],
        "summary": "Obter QR",
        "x-channels": ["whatsmeow", "baileys"],
        "description": "Retorna o QR pendente como STRING do protocolo e como IMAGEM PNG base64 (`qrImage`, pronta para `<img src>`). Vazio se não há QR pendente. Requer `channels:write`.",
        "responses": {
          "200": { "description": "QR", "content": { "application/json": { "schema": { "type": "object", "properties": { "qr": { "type": "string" }, "qrImage": { "type": "string", "description": "data:image/png;base64,… (vazio se sem QR)" } } } } } }
        }
      }
    },
    "/channels/{id}/pair-phone": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "post": {
        "tags": ["Canais"],
        "summary": "Parear por código",
        "x-channels": ["whatsmeow", "baileys"],
        "description": "Alternativa ao QR: gera um código de 8 dígitos para digitar no celular. Depois consulte `GET /channels/{id}/pair-code`. Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["phone"], "properties": { "phone": { "type": "string", "example": "5548999998888" } } } } } },
        "responses": { "202": { "description": "Pareando", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "pairing" } } } } } } }
      }
    },
    "/channels/{id}/pair-code": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "get": {
        "tags": ["Canais"],
        "summary": "Obter código de pareamento",
        "x-channels": ["whatsmeow", "baileys"],
        "responses": { "200": { "description": "Código", "content": { "application/json": { "schema": { "type": "object", "properties": { "pairCode": { "type": "string", "example": "ABCD-1234" } } } } } } }
      }
    },
    "/channels/{id}/disconnect": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "post": {
        "tags": ["Canais"],
        "summary": "Desconectar",
        "x-channels": ["whatsmeow", "baileys"],
        "responses": { "200": { "$ref": "#/components/responses/Ok" } }
      }
    },
    "/media": {
      "post": {
        "tags": ["Mídia"],
        "summary": "Upload de mídia",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Envia um arquivo (multipart/form-data, campo `file`) e recebe uma `key` para usar como `mediaRef` no `POST /messages`. Requer `messages:send`.",
        "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary" } } } } } },
        "responses": {
          "200": { "description": "Arquivo armazenado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MediaUpload" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/messages": {
      "post": {
        "tags": ["Mensagens"],
        "summary": "Enviar mensagem",
        "x-channels": ["whatsmeow", "baileys", "cloud"],
        "description": "Envia uma mensagem NOVA para um número de **WhatsApp** (o campo `to` é o telefone, ex.: `5548999998888`). `type` seleciona o formato: `text`, `image`, `audio`, `video`, `document`, `location`, `contact` (todos os provedores de WhatsApp) e `interactive`, `template` (**só Cloud**). \n\n⚠️ **Instagram e Telegram:** este endpoint atende hoje apenas canais de **WhatsApp** — o endereçamento é por telefone, enquanto Instagram usa IGSID e Telegram usa usuário/ID. Para responder mensagens desses canais (recebidas via webhook `message.received`), use o **`POST /messages/{id}/reply`**, que envia texto/mídia na conversa da mensagem sem precisar de telefone. \n\nA conversa é aberta no inbox no `departmentId` (obrigatório), sem atribuir a um atendente. Para mídia, faça antes o `POST /media` e use a `key` como `mediaRef`. Requer `messages:send`. \n\n📣 **Disparo sem abrir atendimento:** envie `\"openConversation\": false` para NÃO materializar um atendimento no inbox — ideal para notificações e disparos, que senão deixariam o inbox cheio de conversas abertas que ninguém precisa tratar. Nesse modo o `departmentId` é dispensável, a mensagem fica no histórico do contato (marcada como **API** no chat) e, **se o cliente responder, o atendimento abre normalmente**. Se já existir um atendimento aberto com esse contato, a mensagem entra nele — para o atendente não ficar sem ver o que o cliente recebeu. A resposta traz `attendanceOpened` indicando o que aconteceu.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendMessageRequest" },
              "examples": {
                "texto": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "text", "text": "Olá! Seu pedido saiu para entrega." } },
                "imagem": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "image", "mediaRef": "tenant/image/2026/07/uuid.jpg", "mimeType": "image/jpeg", "caption": "Nota fiscal" } },
                "audio": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "audio", "mediaRef": "tenant/audio/2026/07/uuid.ogg", "ptt": true } },
                "localizacao": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "location", "location": { "latitude": -27.5949, "longitude": -48.5482, "name": "Loja Centro", "address": "Rua X, 100" } } },
                "contato": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "contact", "contact": { "name": "Suporte", "phone": "5548333334444" } } },
                "botoes (só Cloud)": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "interactive", "interactive": { "kind": "button", "body": "Confirma o pedido?", "buttons": [ { "id": "sim", "title": "Sim" }, { "id": "nao", "title": "Não" } ] } } },
                "template (só Cloud)": { "value": { "channelId": "1a2b…", "departmentId": "9f8e…", "to": "5548999998888", "type": "template", "template": { "name": "pedido_confirmado", "language": "pt_BR", "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "João" }, { "type": "text", "text": "#4821" } ] } ] } } }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Mensagem enfileirada", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendMessageResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "description": "Número em quarentena (CHANNEL_FLAGGED)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "CHANNEL_NOT_CONNECTED | OUTSIDE_24H_WINDOW (Cloud, fora da janela de 24h — use type=template) | TEMPLATE_UNSUPPORTED/INTERACTIVE_UNSUPPORTED (só Cloud)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Limite anti-ban: COLD_START_LIMIT ou PACING_WAIT (ver Retry-After) ou RATE_LIMIT_EXCEEDED", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/messages/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/MessageID" }],
      "get": {
        "tags": ["Mensagens"],
        "summary": "Status de entrega",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Consulta o status de uma mensagem enviada (polling; ou assine o webhook `message.status`). Requer `messages:send`.",
        "responses": {
          "200": { "description": "Status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageStatus" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "tags": ["Ações"],
        "summary": "Editar mensagem",
        "x-channels": ["whatsmeow", "baileys", "telegram"],
        "description": "Edita o texto de uma mensagem já enviada. **Restrições:** só mensagens de **texto** de **saída** (enviadas por você) e dentro da **janela de ~15 min** após o envio. Suportado em **whatsmeow**, **baileys** e **telegram** — a API oficial (Cloud) e o Instagram não expõem edição. O canal e o chat são derivados da própria mensagem. Requer `messages:send`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["text"], "properties": { "text": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Editada", "content": { "application/json": { "schema": { "type": "object", "properties": { "messageId": { "type": "string", "format": "uuid" }, "status": { "type": "string" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "EDIT_NOT_SUPPORTED (Cloud) | CANNOT_EDIT (não é texto de saída) | EDIT_WINDOW_EXPIRED (>~15 min)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "tags": ["Ações"],
        "summary": "Apagar mensagem (para todos)",
        "x-channels": ["whatsmeow", "baileys", "telegram"],
        "description": "Apaga para todos uma mensagem de saída (\"apagar para todos\"). Só mensagens enviadas por você. Suportado em **whatsmeow**, **baileys** e **telegram** (a API oficial Cloud não expõe revogação). O canal e o chat são derivados da própria mensagem. Requer `messages:send`.",
        "responses": {
          "204": { "description": "Apagada" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "CANNOT_REVOKE (não é mensagem de saída) | REVOKE_NOT_SUPPORTED (canal Cloud)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/messages/{id}/reactions": {
      "parameters": [{ "$ref": "#/components/parameters/MessageID" }],
      "post": {
        "tags": ["Ações"],
        "summary": "Reagir a uma mensagem",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Reage com um emoji a uma mensagem (envie `emoji` vazio para remover). O canal e o chat são derivados da própria mensagem (não é possível reagir a números arbitrários) — funciona em qualquer canal com reações, incluindo **Instagram** e **Telegram**. Requer `messages:send`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "emoji": { "type": "string", "example": "👍", "description": "Vazio remove a reação." } } } } } },
        "responses": { "202": { "description": "Reação enfileirada" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/messages/{id}/reply": {
      "parameters": [{ "$ref": "#/components/parameters/MessageID" }],
      "post": {
        "tags": ["Mensagens"],
        "summary": "Responder a uma mensagem recebida",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Responde com **texto** ou **mídia** a uma mensagem que você recebeu (via webhook `message.received`). Funciona em **todos os canais** — é o caminho para responder **Instagram** e **Telegram**, que o `POST /messages` não atende (aquele endereça por telefone, só WhatsApp).\n\n**Você NÃO precisa (nem pode) escolher o canal aqui.** O único identificador é o `id` da mensagem, no path — a partir dele o sistema descobre automaticamente a conexão e o chat de origem e envia a resposta **de volta pela mesma conexão** que recebeu a mensagem. Por isso o corpo não tem `channelId`, `to` nem `departmentId`: seria impossível responder pelo canal errado. (Para iniciar uma conversa NOVA, aí sim escolha o canal no `POST /messages`.)\n\n`type` aceita `text`, `image`, `audio`, `video`, `document` (para mídia, faça antes o `POST /media` e use a `key` como `mediaRef`). Respeita as janelas do provedor: **Instagram** só permite responder dentro das **24h** desde a última mensagem do usuário; **Telegram** e **WhatsApp** não têm essa limitação. Requer `messages:send`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ReplyMessageRequest" },
              "examples": {
                "texto": { "value": { "type": "text", "text": "Claro! Já estou verificando por aqui." } },
                "imagem": { "value": { "type": "image", "mediaRef": "tenant/image/2026/07/uuid.jpg", "mimeType": "image/jpeg", "caption": "Segue o comprovante" } },
                "audio": { "value": { "type": "audio", "mediaRef": "tenant/audio/2026/07/uuid.ogg", "ptt": true } }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Resposta enfileirada", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendMessageResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "description": "Canal em quarentena/banido (CHANNEL_FLAGGED)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "CHANNEL_NOT_CONNECTED | OUTSIDE_24H_WINDOW (Instagram/Cloud fora da janela de 24h)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/typing": {
      "post": {
        "tags": ["Ações"],
        "summary": "Indicador \"digitando\"",
        "x-channels": ["cloud"],
        "description": "Mostra o indicador \"digitando\" ao contato. Só **Cloud** (nos não-oficiais a digitação é simulada pelo worker; a chamada é aceita como no-op). Requer `messages:send`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PresenceRequest" } } } },
        "responses": { "202": { "description": "Aceito" } }
      }
    },
    "/read": {
      "post": {
        "tags": ["Ações"],
        "summary": "Marcar como lida",
        "x-channels": ["cloud"],
        "description": "Marca as mensagens recebidas do contato como lidas (tique azul). Só **Cloud** (nos não-oficiais o worker já cuida da leitura; a chamada é aceita como no-op). Requer `messages:send`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PresenceRequest" } } } },
        "responses": { "202": { "description": "Aceito" } }
      }
    },
    "/departments": {
      "get": {
        "tags": ["Utilidades"],
        "summary": "Departamentos da empresa",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram", "webchat"],
        "description": "Lista os departamentos da empresa. Use o `id` em `departmentId` ao enviar mensagem abrindo atendimento (`openConversation` ausente ou `true`). Requer `channels:read`.",
        "responses": {
          "200": {
            "description": "Departamentos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "departments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "format": "uuid" },
                          "name": { "type": "string", "example": "Vendas" },
                          "color": { "type": "string", "example": "#00BA92" },
                          "description": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/capabilities": {
      "get": {
        "tags": ["Utilidades"],
        "summary": "Matriz de recursos por provedor",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram", "webchat"],
        "description": "Retorna o que cada provedor suporta (templates, interativas, localização, contato, reação, digitação, marcar-lida, mídia…). Útil para descobrir os recursos antes de tentar. Requer `channels:read`.",
        "responses": { "200": { "description": "Capacidades", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Capabilities" } } } } }
      }
    },
    "/channels/{id}/templates": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "get": {
        "tags": ["Templates"],
        "summary": "Listar templates (Cloud)",
        "x-channels": ["cloud"],
        "description": "Lista os templates da WABA (cache local). Só canais Cloud. Requer `channels:read`.\n\n**Por padrão devolve TODOS**, inclusive `PENDING` e `REJECTED` — é o que permite acompanhar a aprovação de um template recém-submetido e ler o `rejectionReason` de um recusado.\n\nSó dá para ENVIAR (`POST /messages` com `type=template`) o template que atende às **duas** condições: `status = APPROVED` **e** `enabled = true` (a flag `enabled` é local — o administrador pode tirar um template aprovado de circulação sem excluí-lo na Meta).\n\nUse **`?sendableOnly=true`** para receber já filtrado só o que é enviável agora, em vez de reimplementar essa regra do seu lado.",
        "parameters": [
          { "name": "sendableOnly", "in": "query", "required": false, "schema": { "type": "boolean", "default": false },
            "description": "`true` devolve apenas os enviáveis (`APPROVED` + `enabled`). Padrão `false` = todos, com o status de cada um." }
        ],
        "responses": { "200": { "description": "Templates do canal", "content": { "application/json": { "schema": { "type": "object", "properties": { "templates": { "type": "array", "items": { "$ref": "#/components/schemas/CloudTemplate" } } } } } } } }
      },
      "post": {
        "tags": ["Templates"],
        "summary": "Submeter template para aprovação (Cloud)",
        "x-channels": ["cloud"],
        "description": "Cria um template HSM na WABA para aprovação da Meta. Para header de mídia, suba antes a amostra em `POST /channels/{id}/templates/sample` e use o `handle`. Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TemplateSubmitRequest" } } } },
        "responses": {
          "201": { "description": "Template submetido (fica PENDING)", "content": { "application/json": { "schema": { "type": "object", "properties": { "template": { "$ref": "#/components/schemas/CloudTemplate" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/channels/{id}/templates/sync": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "post": {
        "tags": ["Templates"],
        "summary": "Sincronizar templates da Meta (Cloud)",
        "x-channels": ["cloud"],
        "description": "Puxa os templates da WABA na Meta e atualiza o cache local. Requer `channels:write`.",
        "responses": {
          "200": { "description": "Sincronizado", "content": { "application/json": { "schema": { "type": "object", "properties": { "synced": { "type": "integer" } } } } } },
          "409": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/channels/{id}/templates/sample": {
      "parameters": [{ "$ref": "#/components/parameters/ChannelID" }],
      "post": {
        "tags": ["Templates"],
        "summary": "Upload de amostra de mídia (header)",
        "x-channels": ["cloud"],
        "description": "Sobe uma amostra de mídia (multipart, campo `file`) à Meta e devolve o `handle` para usar no header de mídia da submissão do template. Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary" } } } } } },
        "responses": { "200": { "description": "Handle da amostra", "content": { "application/json": { "schema": { "type": "object", "properties": { "handle": { "type": "string" } } } } } } }
      }
    },
    "/templates/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/TemplateID" }],
      "put": {
        "tags": ["Templates"],
        "summary": "Editar template (Cloud)",
        "x-channels": ["cloud"],
        "description": "Edita os componentes de um template (nome/idioma/categoria são imutáveis; o template volta para PENDING). Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TemplateSubmitRequest" } } } },
        "responses": {
          "200": { "description": "Template editado", "content": { "application/json": { "schema": { "type": "object", "properties": { "template": { "$ref": "#/components/schemas/CloudTemplate" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "Template não editável (TEMPLATE_NOT_EDITABLE)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "tags": ["Templates"],
        "summary": "Flags do template (ativar / campanha)",
        "x-channels": ["cloud"],
        "description": "Atualiza flags LOCAIS (não tocam na Meta): `enabled` (desativado some do seletor de envio) e `isCampaign` (apto ao seletor de Disparos). Informe ao menos uma. Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "isCampaign": { "type": "boolean" } } } } } },
        "responses": { "200": { "$ref": "#/components/responses/Ok" }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": {
        "tags": ["Templates"],
        "summary": "Excluir template (Cloud)",
        "x-channels": ["cloud"],
        "description": "Apaga o template na Meta (todas as línguas do nome) e no cache local. **Destrutivo e irreversível na Meta.** Requer `channels:write`.",
        "responses": { "200": { "description": "Excluído", "content": { "application/json": { "schema": { "type": "object", "properties": { "deleted": { "type": "string", "description": "Nome do template excluído." } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/webhooks": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Registrar webhook",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Registra um endpoint HTTPS que recebe os eventos assinados. Devolve o `secret` (para validar a assinatura HMAC) **uma única vez**. Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWebhookRequest" }, "example": { "url": "https://meusistema.com/webhooks/digiwhats", "events": ["message.received", "message.status"], "description": "ERP produção" } } } },
        "responses": {
          "201": { "description": "Webhook registrado (com o secret)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "description": "Limite de webhooks por empresa atingido (WEBHOOK_LIMIT)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "tags": ["Webhooks"],
        "summary": "Listar webhooks",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Lista os endpoints registrados (sem o secret) com contadores de saúde. Requer `channels:read`.",
        "responses": { "200": { "description": "Webhooks", "content": { "application/json": { "schema": { "type": "object", "properties": { "webhooks": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEndpoint" } } } } } } } }
      }
    },
    "/webhooks/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/WebhookID" }],
      "get": {
        "tags": ["Webhooks"],
        "summary": "Consultar webhook",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Detalhe de um endpoint (sem o secret). Requer `channels:read`.",
        "responses": { "200": { "description": "Webhook", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "patch": {
        "tags": ["Webhooks"],
        "summary": "Atualizar webhook",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Atualiza url, events, enabled e/ou description (PATCH parcial). Requer `channels:write`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateWebhookRequest" } } } },
        "responses": { "200": { "$ref": "#/components/responses/Ok" }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": {
        "tags": ["Webhooks"],
        "summary": "Excluir webhook",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Remove o endpoint (para de receber eventos). Requer `channels:write`.",
        "responses": { "204": { "description": "Excluído" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/webhooks/{id}/redrive": {
      "parameters": [{ "$ref": "#/components/parameters/WebhookID" }],
      "post": {
        "tags": ["Webhooks"],
        "summary": "Reprocessar entregas mortas",
        "x-channels": ["whatsmeow", "baileys", "cloud", "instagram", "telegram"],
        "description": "Volta as entregas com status `dead` deste endpoint para a fila (`pending`, tentativas zeradas). Use após consertar o destino; se o endpoint foi auto-desativado por falhas seguidas, reative-o antes (`PATCH /webhooks/{id}` com `enabled=true`). Requer `channels:write`.",
        "responses": {
          "200": { "description": "Reprocessadas", "content": { "application/json": { "schema": { "type": "object", "properties": { "redriven": { "type": "integer", "description": "Quantas entregas voltaram para a fila." } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "Authorization: Bearer dwuser_live_…" },
      "apiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "X-API-Key: dwuser_live_…" }
    },
    "parameters": {
      "ChannelID": { "name": "id", "in": "path", "required": true, "description": "ID do canal (UUID)", "schema": { "type": "string", "format": "uuid" } },
      "MessageID": { "name": "id", "in": "path", "required": true, "description": "ID da mensagem (UUID retornado no envio)", "schema": { "type": "string", "format": "uuid" } },
      "TemplateID": { "name": "id", "in": "path", "required": true, "description": "ID do template (UUID local)", "schema": { "type": "string", "format": "uuid" } },
      "WebhookID": { "name": "id", "in": "path", "required": true, "description": "ID do webhook (UUID)", "schema": { "type": "string", "format": "uuid" } }
    },
    "responses": {
      "Ok": { "description": "Sucesso", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "example": true } } } } } },
      "BadRequest": { "description": "Requisição inválida", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Não encontrado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": { "type": "object", "properties": { "code": { "type": "string", "example": "CHANNEL_FLAGGED" }, "message": { "type": "string", "example": "Número em risco de banimento — apenas responda as mensagens que chegarem." } } },
      "CreateChannelRequest": {
        "type": "object", "required": ["name", "provider"],
        "properties": {
          "name": { "type": "string", "example": "API Vendas" },
          "provider": { "type": "string", "enum": ["whatsmeow", "baileys"], "description": "Provedor não-oficial (conecta por QR/código). Para a API oficial (Cloud), use POST /channels/cloud.", "example": "whatsmeow" },
          "proxyId": { "type": "string", "nullable": true, "description": "Opcional — proxy dedicado (anti-ban)." }
        }
      },
      "CloudChannelRequest": {
        "type": "object", "required": ["name", "phoneNumberId", "accessToken"],
        "properties": {
          "name": { "type": "string", "example": "Vendas (Cloud)" },
          "phoneNumberId": { "type": "string", "description": "Phone Number ID do número na Meta.", "example": "120376389012345" },
          "accessToken": { "type": "string", "description": "Access token permanente (System User) da Meta, com permissão no número." },
          "wabaId": { "type": "string", "nullable": true, "description": "WhatsApp Business Account ID (opcional)." }
        }
      },
      "Channel": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "provider": { "type": "string" },
          "status": { "type": "string", "enum": ["created", "connecting", "connected", "disconnected", "suspended"] },
          "phone": { "type": "string", "nullable": true },
          "flagStatus": { "type": "string", "nullable": true, "enum": ["at_risk", "banned"], "description": "Quarentena anti-ban (quando presente)." }
        }
      },
      "MediaUpload": {
        "type": "object",
        "properties": {
          "key": { "type": "string", "description": "Use como mediaRef no POST /messages." },
          "url": { "type": "string" },
          "type": { "type": "string", "enum": ["image", "audio", "video", "document"] },
          "mimeType": { "type": "string" },
          "size": { "type": "integer" },
          "filename": { "type": "string" }
        }
      },
      "PresenceRequest": {
        "type": "object", "required": ["channelId", "to"],
        "properties": {
          "channelId": { "type": "string", "format": "uuid" },
          "to": { "type": "string", "description": "Número E.164 do contato. Ex.: 5548999998888." }
        }
      },
      "ReplyMessageRequest": {
        "type": "object", "required": ["type"],
        "description": "Corpo da resposta a uma mensagem recebida. Só o conteúdo — o canal e o chat vêm do `id` da mensagem (path), nunca do corpo. Por isso NÃO existem `channelId`, `to` nem `departmentId` aqui.",
        "properties": {
          "type": { "type": "string", "enum": ["text", "image", "audio", "video", "document"], "default": "text" },
          "text": { "type": "string", "description": "Obrigatório para type=text." },
          "mediaRef": { "type": "string", "description": "key do POST /media (mídia)." },
          "mimeType": { "type": "string" },
          "caption": { "type": "string" },
          "filename": { "type": "string" },
          "ptt": { "type": "boolean", "description": "Áudio como nota de voz." }
        }
      },
      "SendMessageRequest": {
        "type": "object", "required": ["channelId", "to", "type"],
        "properties": {
          "channelId": { "type": "string", "format": "uuid" },
          "departmentId": { "type": "string", "format": "uuid", "description": "A conversa é aberta neste departamento. Obrigatório, EXCETO quando openConversation=false. Os ids saem de GET /departments." },
          "openConversation": { "type": "boolean", "default": true, "description": "false = DISPARO: envia sem abrir atendimento no inbox (a mensagem vai para o histórico do contato e a resposta dele abre o atendimento normalmente). Ausente ou true = abre a conversa, como sempre." },
          "to": { "type": "string", "description": "Número E.164 (país+DDD+número). Ex.: 5548999998888." },
          "type": { "type": "string", "enum": ["text", "image", "audio", "video", "document", "location", "contact", "interactive", "template"], "default": "text" },
          "text": { "type": "string", "description": "Obrigatório para type=text." },
          "mediaRef": { "type": "string", "description": "key do POST /media (mídia)." },
          "mimeType": { "type": "string" },
          "caption": { "type": "string" },
          "filename": { "type": "string" },
          "ptt": { "type": "boolean", "description": "Áudio como nota de voz." },
          "location": { "$ref": "#/components/schemas/Location", "description": "Obrigatório para type=location." },
          "contact": { "$ref": "#/components/schemas/ContactCard", "description": "Obrigatório para type=contact." },
          "interactive": { "$ref": "#/components/schemas/InteractiveMessage", "description": "Obrigatório para type=interactive. Só canais da API oficial (Cloud)." },
          "template": { "$ref": "#/components/schemas/TemplateMessage", "description": "Obrigatório para type=template. Só Cloud; permite iniciar conversa fora da janela de 24h com um template APROVADO." }
        }
      },
      "Location": {
        "type": "object", "required": ["latitude", "longitude"],
        "properties": {
          "latitude": { "type": "number", "example": -27.5949 },
          "longitude": { "type": "number", "example": -48.5482 },
          "name": { "type": "string" },
          "address": { "type": "string" }
        }
      },
      "ContactCard": {
        "type": "object",
        "description": "Cartão de contato (type=contact). Informe name+phone OU um vCard cru (vcard tem precedência).",
        "properties": {
          "name": { "type": "string" },
          "phone": { "type": "string", "description": "Número E.164 do contato compartilhado." },
          "email": { "type": "string" },
          "vcard": { "type": "string", "description": "vCard 3.0 cru (opcional; se presente, é usado como está)." }
        }
      },
      "TemplateMessage": {
        "type": "object",
        "description": "Envio de um template APROVADO na Meta (mensagem ativa, fora da janela de 24h). name+language identificam o template; components preenchem os placeholders.",
        "required": ["name", "language"],
        "properties": {
          "name": { "type": "string" },
          "language": { "type": "string", "example": "pt_BR" },
          "components": {
            "type": "array",
            "items": {
              "type": "object", "required": ["type"],
              "properties": {
                "type": { "type": "string", "enum": ["header", "body", "button"] },
                "subType": { "type": "string", "enum": ["quick_reply", "url", "copy_code"], "description": "Só para type=button." },
                "index": { "type": "string", "description": "Posição do botão (0-based), só type=button." },
                "parameters": {
                  "type": "array",
                  "items": {
                    "type": "object", "required": ["type"],
                    "properties": {
                      "type": { "type": "string", "enum": ["text", "currency", "date_time", "image", "video", "document", "location", "payload", "coupon_code", "action"] },
                      "text": { "type": "string" },
                      "parameterName": { "type": "string", "description": "Params nomeados no body ({{nome}})." },
                      "currencyCode": { "type": "string" },
                      "amount1000": { "type": "integer" },
                      "mediaId": { "type": "string" },
                      "mediaLink": { "type": "string" },
                      "filename": { "type": "string" },
                      "payload": { "type": "string" },
                      "couponCode": { "type": "string" },
                      "location": { "type": "object", "description": "type=location.", "properties": { "latitude": { "type": "number" }, "longitude": { "type": "number" }, "name": { "type": "string" }, "address": { "type": "string" } } },
                      "flowToken": { "type": "string", "description": "type=action (botão de Flow): correlaciona a resposta (nfm_reply)." },
                      "flowActionData": { "type": "object", "description": "type=action: dados iniciais do Flow disparado pelo botão." }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "InteractiveMessage": {
        "type": "object",
        "description": "Mensagem interativa — EXCLUSIVO de canais Cloud. kind: button (1–3 botões), list (seções/linhas), cta_url, location_request, flow, product/product_list/catalog. body obrigatório exceto product.",
        "required": ["kind"],
        "properties": {
          "kind": { "type": "string", "enum": ["button", "list", "cta_url", "location_request", "flow", "product", "product_list", "catalog"] },
          "header": { "type": "string" },
          "body": { "type": "string" },
          "footer": { "type": "string" },
          "buttons": { "type": "array", "description": "kind=button: 1–3 botões.", "items": { "type": "object", "required": ["id", "title"], "properties": { "id": { "type": "string" }, "title": { "type": "string" } } } },
          "buttonText": { "type": "string", "description": "kind=list: rótulo do botão." },
          "sections": { "type": "array", "description": "kind=list.", "items": { "type": "object", "required": ["rows"], "properties": { "title": { "type": "string" }, "rows": { "type": "array", "items": { "type": "object", "required": ["id", "title"], "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" } } } } } } },
          "ctaDisplayText": { "type": "string", "description": "kind=cta_url." },
          "ctaUrl": { "type": "string", "description": "kind=cta_url." },
          "flow": { "type": "object", "description": "kind=flow.", "required": ["flowToken", "flowCta"], "properties": { "flowToken": { "type": "string" }, "flowId": { "type": "string" }, "flowName": { "type": "string" }, "flowCta": { "type": "string" }, "mode": { "type": "string", "enum": ["draft", "published"] }, "flowAction": { "type": "string", "enum": ["navigate", "data_exchange"] }, "flowActionPayload": { "type": "object" } } },
          "catalogId": { "type": "string" },
          "productRetailerId": { "type": "string" },
          "thumbnailProductId": { "type": "string" },
          "productSections": { "type": "array", "items": { "type": "object", "required": ["productRetailerIds"], "properties": { "title": { "type": "string" }, "productRetailerIds": { "type": "array", "items": { "type": "string" } } } } }
        }
      },
      "TemplateSubmitRequest": {
        "type": "object",
        "description": "Definição de um template HSM para submeter/editar na Meta. Categorias: MARKETING/UTILITY (header opcional, body com variáveis, footer, botões) e AUTHENTICATION (OTP — body/footer gerados pela Meta, controle via auth).",
        "required": ["name", "language", "category"],
        "properties": {
          "name": { "type": "string", "description": "Só letras minúsculas, números e _ (até 512).", "example": "pedido_confirmado" },
          "language": { "type": "string", "example": "pt_BR" },
          "category": { "type": "string", "enum": ["MARKETING", "UTILITY", "AUTHENTICATION"] },
          "body": { "type": "string", "description": "Corpo (MARKETING/UTILITY). Variáveis {{1}} (posicional) ou {{nome}} (nomeado) — não misture.", "example": "Olá {{1}}, seu pedido {{2}} foi confirmado." },
          "bodyExample": { "type": "array", "description": "POSITIONAL: exemplos das variáveis do body, ex.: [[\"João\",\"#4821\"]].", "items": { "type": "array", "items": { "type": "string" } } },
          "parameterFormat": { "type": "string", "enum": ["POSITIONAL", "NAMED"], "description": "\"\"/POSITIONAL usa {{1}}; NAMED usa {{nome}} + bodyNamedExamples." },
          "bodyNamedExamples": { "type": "array", "items": { "$ref": "#/components/schemas/NamedExample" } },
          "footer": { "type": "string" },
          "header": {
            "type": "object", "description": "Header opcional.",
            "properties": {
              "format": { "type": "string", "enum": ["TEXT", "IMAGE", "VIDEO", "DOCUMENT"] },
              "text": { "type": "string", "description": "Header TEXT (pode ter 1 variável)." },
              "example": { "type": "array", "items": { "type": "string" }, "description": "POSITIONAL: exemplo do header TEXT." },
              "namedExamples": { "type": "array", "items": { "$ref": "#/components/schemas/NamedExample" } },
              "handle": { "type": "string", "description": "header_handle da amostra (POST .../templates/sample) para header de mídia." }
            }
          },
          "buttons": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": ["QUICK_REPLY", "URL", "PHONE_NUMBER", "COPY_CODE"] }, "text": { "type": "string" }, "url": { "type": "string", "description": "Botão URL; use {{1}} para a parte variável do link." }, "phoneNumber": { "type": "string" }, "example": { "type": "array", "items": { "type": "string" }, "description": "Exemplo(s) da parte variável do botão URL (exigido pela Meta quando a URL tem variável)." } } } },
          "auth": {
            "type": "object", "description": "Só category=AUTHENTICATION (OTP).",
            "properties": {
              "securityRecommendation": { "type": "boolean" },
              "codeExpirationMinutes": { "type": "integer", "description": "1–90; 0 = sem rodapé de expiração." },
              "otpType": { "type": "string", "enum": ["COPY_CODE", "ONE_TAP"] },
              "buttonText": { "type": "string" },
              "autofillText": { "type": "string", "description": "ONE_TAP: texto do botão de autofill (Android)." },
              "packageName": { "type": "string", "description": "ONE_TAP: package do app Android." },
              "signatureHash": { "type": "string", "description": "ONE_TAP: hash de assinatura do app (autofill)." }
            }
          },
          "isCampaign": { "type": "boolean", "description": "Marca o template como de campanha (aparece no seletor de Disparos)." }
        }
      },
      "NamedExample": {
        "type": "object", "required": ["name", "example"],
        "properties": { "name": { "type": "string", "example": "nome" }, "example": { "type": "string", "example": "João" } }
      },
      "CloudTemplate": {
        "type": "object",
        "description": "Template de mensagem da WABA (cache local, sincronizado da Meta).",
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "UUID local do template — use nas rotas PUT/PATCH/DELETE /templates/{id}." },
          "metaId": { "type": "string", "description": "ID do template na Meta." },
          "name": { "type": "string" },
          "language": { "type": "string", "example": "pt_BR" },
          "status": { "type": "string", "enum": ["APPROVED", "PENDING", "REJECTED", "PAUSED", "DISABLED"], "description": "Status na Meta. Só `APPROVED` pode ser enviado — e ainda depende de `enabled`." },
          "category": { "type": "string", "enum": ["MARKETING", "UTILITY", "AUTHENTICATION"] },
          "rejectionReason": { "type": "string", "nullable": true, "description": "Motivo da recusa quando `status = REJECTED`." },
          "enabled": { "type": "boolean", "description": "Flag LOCAL (não vem da Meta): o administrador pode tirar de circulação um template aprovado. `false` = não enviar, mesmo APPROVED." },
          "isCampaign": { "type": "boolean", "description": "Marcado para uso em DISPAROS em massa. Não é gate de envio — é a intenção de uso, para separar do atendimento individual." },
          "components": { "type": "array", "items": { "type": "object" } },
          "syncedAt": { "type": "string", "format": "date-time" }
        }
      },
      "SendMessageResponse": {
        "type": "object",
        "properties": {
          "messageId": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "example": "pending" },
          "to": { "type": "string" },
          "contactId": { "type": "string", "format": "uuid", "description": "Contato resolvido/criado a partir do número." },
          "attendanceOpened": { "type": "boolean", "description": "true = este envio materializou o atendimento no inbox. false = disparo (openConversation:false)." },
          "conversationId": { "type": "string", "format": "uuid", "nullable": true, "description": "Atendimento vinculado. Vazio quando o disparo não tocou em nenhuma conversa; preenchido mesmo com attendanceOpened=false se já existia um atendimento aberto com o contato." }
        }
      },
      "MessageStatus": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["pending", "sent", "delivered", "read", "failed"] },
          "providerMsgId": { "type": "string" },
          "type": { "type": "string" },
          "to": { "type": "string" },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "Capabilities": {
        "type": "object",
        "description": "Mapa provedor → capacidades. providers.<whatsmeow|baileys|cloud> com flags booleanas. ATENÇÃO: typingIndicator/readReceipts descrevem o COMPORTAMENTO do provedor (nos não-oficiais o worker já faz digitação/leitura automaticamente) — os endpoints POST /typing e POST /read só têm efeito em canais cloud (ver x-channels).",
        "properties": {
          "providers": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "templates": { "type": "boolean" },
                "interactive": { "type": "boolean" },
                "location": { "type": "boolean" },
                "contacts": { "type": "boolean" },
                "reactions": { "type": "boolean" },
                "readReceipts": { "type": "boolean" },
                "typingIndicator": { "type": "boolean" },
                "presence": { "type": "boolean" },
                "requiresQr": { "type": "boolean" },
                "stateless": { "type": "boolean" },
                "proxy": { "type": "boolean" },
                "mediaTypes": { "type": "array", "items": { "type": "string" } }
              }
            }
          }
        }
      },
      "CreateWebhookRequest": {
        "type": "object", "required": ["url"],
        "properties": {
          "url": { "type": "string", "description": "URL https de destino.", "example": "https://meusistema.com/webhooks/digiwhats" },
          "events": { "type": "array", "items": { "type": "string", "enum": ["message.received", "message.status"] }, "description": "Default: ambos os eventos." },
          "description": { "type": "string" }
        }
      },
      "UpdateWebhookRequest": {
        "type": "object",
        "description": "PATCH parcial — envie só os campos a alterar.",
        "properties": {
          "url": { "type": "string" },
          "events": { "type": "array", "items": { "type": "string", "enum": ["message.received", "message.status"] } },
          "enabled": { "type": "boolean" },
          "description": { "type": "string" }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string" },
          "events": { "type": "array", "items": { "type": "string" } },
          "enabled": { "type": "boolean" },
          "description": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" },
          "consecutiveFailures": { "type": "integer" },
          "lastStatusCode": { "type": "integer", "nullable": true },
          "secret": { "type": "string", "description": "Segredo de assinatura HMAC — retornado APENAS na criação. Guarde-o." }
        }
      }
    }
  }
}
