{
  "openapi": "3.1.0",
  "info": {
    "title": "FluxoAPI",
    "version": "1.0.0",
    "summary": "WhatsApp Cloud API oficial, com coexistencia.",
    "description": "A FluxoAPI conecta o número de WhatsApp da sua empresa à **Cloud API oficial da Meta**, com\ncoexistência: o número continua funcionando no aplicativo WhatsApp Business do celular e, ao\nmesmo tempo, responde por esta API. Sem QR Code, sem navegador headless, sem risco de banimento\npor uso não oficial.\n\n## Comece por aqui\n\n1. Crie uma instância no painel e conecte o número (Embedded Signup da Meta).\n2. Em **Credenciais**, gere uma chave. A chave e o segredo aparecem **uma única vez**.\n3. Assine a requisição (receita abaixo) e chame `GET /instances` para confirmar.\n\nBase: `https://api.fluxoapi.com.br/v1`\n\n## Autenticação — chave + assinatura HMAC\n\nToda requisição leva quatro cabeçalhos:\n\n| Cabeçalho | Conteúdo |\n|---|---|\n| `X-Fluxo-Key` | sua chave, com prefixo `fxk_live_` ou `fxk_test_` |\n| `X-Fluxo-Timestamp` | segundos desde a época (Unix), como texto |\n| `X-Fluxo-Nonce` | valor único por requisição, 8 a 128 caracteres |\n| `X-Fluxo-Signature` | `v1=` seguido do HMAC-SHA256 em hexadecimal |\n\nA **string assinada** são seis linhas unidas por `\\n`, nesta ordem exata:\n\n```\nv1\n{timestamp}\n{nonce}\n{MÉTODO EM MAIÚSCULAS}\n{caminho com query, exatamente como enviado}\n{sha256 do corpo, em hexadecimal}\n```\n\nO corpo entra pelo **hash**, não pelo conteúdo. Requisição sem corpo usa o sha256 da string\nvazia: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.\n\nAssine e envie os **mesmos bytes**. Serializar o JSON duas vezes muda a ordem das chaves e a\nassinatura não bate — é a causa nº 1 de `invalid_signature`.\n\n```js\nimport { createHmac, createHash } from 'node:crypto';\n\nconst body = JSON.stringify({ to: '+5511999999999', type: 'text', text: { body: 'Oi' } });\nconst ts = String(Math.floor(Date.now() / 1000));\nconst nonce = crypto.randomUUID();\nconst path = '/v1/instances/inst_01J.../messages';   // com a query, se houver\n\nconst assinada = ['v1', ts, nonce, 'POST', path,\n  createHash('sha256').update(body).digest('hex')].join('\\n');\n\nconst headers = {\n  'content-type': 'application/json',\n  'X-Fluxo-Key': process.env.FLUXO_KEY,\n  'X-Fluxo-Timestamp': ts,\n  'X-Fluxo-Nonce': nonce,\n  'X-Fluxo-Signature': 'v1=' + createHmac('sha256', process.env.FLUXO_SECRET)\n    .update(assinada).digest('hex'),\n};\nawait fetch('https://api.fluxoapi.com.br' + path, { method: 'POST', headers, body });\n```\n\n**Relógio e repetição.** O carimbo vale 5 minutos para cada lado; servidor sem NTP falha com\n`stale_timestamp`. O nonce fica registrado por 10 minutos: repetir devolve `409 nonce_reused`.\nAssinatura sozinha não impede reenvio — e cada reenvio cobraria de novo.\n\n## Permissões da chave\n\nCada credencial carrega escopos e pode ser presa a **uma instância**. Chave vazada de uma\ninstância não alcança os outros números da conta.\n\n| Escopo | Dá acesso a |\n|---|---|\n| `messages:send` | enviar texto, mídia, botões, lista, carrossel e modelo; marcar como lida |\n| `messages:read` | ler mensagens |\n| `conversations:read` | listar conversas e a janela de 24 h |\n| `templates:read` | ler o catálogo de modelos aprovados |\n| `templates:write` | criar modelo e subir mídia de cabeçalho (`messages:send` também libera) |\n| `funnel:read` | ler oportunidades |\n| `funnel:write` | mover oportunidade de etapa |\n\nFaltando o escopo: `403 insufficient_scope`, e o campo `required_scope` diz qual.\n\n## A janela de 24 horas\n\nRegra da Meta, não nossa: você responde livremente por **24 h após a última mensagem do\ncontato**. Fora disso, apenas **modelo aprovado**.\n\n- `POST /instances/{instance_id}/messages` recusa com `422 window_closed` e devolve\n  `window_expired_at`.\n- `POST /instances/{instance_id}/messages/template` funciona sempre — e é **sempre cobrado**\n  pela Meta. A resposta traz `billable: true`, para você saber antes da fatura.\n- `GET /conversations` traz `window_expires_at` em cada conversa.\n\n## Idempotência\n\nMande `idempotency_key` no corpo dos envios. A mesma chave, dentro de **7 dias**, devolve a\n**mesma mensagem** em vez de enviar de novo: resposta `200` com o cabeçalho\n`x-fluxo-idempotent-replay: true` e `replayed: true` no corpo. Sem ela, uma retentativa da sua\ninfraestrutura vira mensagem duplicada no celular do cliente.\n\n## Limites de uso\n\nPor credencial, em janela de 1 minuto:\n\n| Plano | Requisições/min |\n|---|---|\n| Starter | 60 |\n| Pro | 300 |\n| Escala | 1.200 |\n| Enterprise | 6.000 |\n\nToda resposta traz `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`. Ao\nestourar, `429 rate_limited` com `Retry-After` em segundos — **respeite o cabeçalho**, não faça\nlaço curto. Requisições que não autenticam têm limite próprio por origem; ao exceder,\n`429 too_many_failures`.\n\n## Envio é assíncrono\n\n`POST` de mensagem responde **202 Accepted** com `status: \"queued\"`. A entrega acontece numa fila\nque respeita a vazão do número. Acompanhe pelo webhook `message.status` ou por `GET /messages`.\n\nO estado avança por posto (`status_rank`) e **nunca retrocede**: `queued`(0) → `sent`(1) →\n`delivered`(2) → `read`(3); `failed` é 9. Status da Meta chega fora de ordem, e um `delivered`\natrasado não desfaz um `read` que já chegou.\n\n## Formato dos erros\n\n```json\n{ \"error\": { \"code\": \"window_closed\",\n             \"message\": \"Texto em português, para humano.\",\n             \"request_id\": \"req_a1b2c3d4\" } }\n```\n\nTrate o `code`, mostre a `message`, guarde o `request_id` — é por ele que se acha a requisição no\nsuporte. Alguns erros trazem campos extras: `required_scope`, `window_expired_at`,\n`current_stage`, `meta_code`.\n\n## Identificadores\n\nULID com prefixo, ordenável por tempo: `inst_`, `conv_`, `msg_`, `cont_`, `lead_`, `cred_`.\nChaves: `fxk_live_…` e `fxk_test_…`; segredos: `fxs_…`; segredo de webhook: `whsec_…`.\n\n## Paginação\n\n`GET /conversations` usa cursor: mande `cursor` com o `next_cursor` da resposta anterior. É\ncarimbo de tempo codificado, não deslocamento — mensagem chegando durante a paginação não faz\npular nem repetir linha. `next_cursor: null` significa fim.\n\n`GET /messages` limita a janela a **90 dias por consulta** (`422 range_too_wide` se exceder),\nporque a tabela é particionada por mês.\n\n## Saúde e versão\n\n`GET https://api.fluxoapi.com.br/health` responde sem autenticação, com `status`, `db`, `redis` e\n`version`. A API é versionada no caminho (`/v1`); mudança incompatível vira `/v2`.",
    "contact": {
      "name": "Suporte FluxoAPI",
      "url": "https://fluxoapi.com.br"
    },
    "license": {
      "name": "Proprietária",
      "url": "https://fluxoapi.com.br/termos"
    },
    "termsOfService": "https://fluxoapi.com.br/termos"
  },
  "servers": [
    {
      "url": "https://api.fluxoapi.com.br/v1",
      "description": "Producao"
    },
    {
      "url": "https://mcp.fluxoapi.com.br",
      "description": "Servidor MCP (agentes de IA)"
    },
    {
      "url": "https://auth.fluxoapi.com.br",
      "description": "Authorization Server (OAuth 2.1)"
    }
  ],
  "components": {
    "securitySchemes": {
      "fluxoHmac": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Fluxo-Key",
        "description": "Chave pública da credencial (`fxk_live_…`). Acompanha SEMPRE outros três cabeçalhos:\\n\\n- `X-Fluxo-Timestamp` — Unix em segundos, tolerância de ±300 s\\n- `X-Fluxo-Nonce` — único por requisição, 8 a 128 caracteres, memória de 600 s\\n- `X-Fluxo-Signature` — `v1=<hex>` do HMAC-SHA256 sobre a string assinada\\n\\nString assinada, seis linhas unidas por barra-n: `v1`, `timestamp`, `nonce`, `MÉTODO`, `caminho+query`, `sha256(corpo)` em hexadecimal.\\n\\nO segredo (`fxs_…`) **nunca** trafega: só a assinatura vai na requisição."
      },
      "mcpBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Token do OAuth 2.1 obtido pelo fluxo do Authorization Server. A audiência é conferida contra o próprio endereço do servidor MCP (RFC 8707): token emitido para outro recurso não vale aqui."
      }
    },
    "schemas": {
      "TemplateDraftButton": {
        "type": "object",
        "required": ["type", "text"],
        "properties": {
          "type": { "type": "string", "enum": ["QUICK_REPLY", "URL", "PHONE_NUMBER", "COPY_CODE", "FLOW", "OTP"] },
          "text": { "type": "string", "maxLength": 25, "description": "Rótulo do botão (a Meta aceita até 25)." },
          "url": { "type": "string", "description": "URL. Pode terminar em `{{1}}` para trecho variável — só no fim." },
          "urlExample": { "type": "string", "description": "Exemplo do trecho variável da URL. Obrigatório quando a URL tem `{{1}}`." },
          "phone_number": { "type": "string", "description": "PHONE_NUMBER: telefone com código do país." },
          "example": { "type": "string", "description": "COPY_CODE: cupom de exemplo." },
          "flow_id": { "type": "string" },
          "flow_action": { "type": "string" },
          "otp_type": { "type": "string", "enum": ["COPY_CODE", "ONE_TAP"] }
        }
      },
      "TemplateDraft": {
        "type": "object",
        "required": ["name", "language", "category"],
        "description": "A receita de um modelo novo — a mesma que o painel usa. A FluxoAPI valida e monta os `components` da Meta.",
        "properties": {
          "name": { "type": "string", "maxLength": 512, "examples": ["pedido_enviado"] },
          "language": { "type": "string", "examples": ["pt_BR"] },
          "category": { "type": "string", "enum": ["MARKETING", "UTILITY", "AUTHENTICATION"] },
          "parameterFormat": { "type": "string", "enum": ["POSITIONAL", "NAMED"], "default": "POSITIONAL", "description": "`NAMED` usa `{{nome}}` em vez de `{{1}}`." },
          "header": {
            "description": "`{ format: \"TEXT\", text, example? }`, `{ format: \"IMAGE\"|\"VIDEO\"|\"DOCUMENT\", handle }` ou `{ format: \"LOCATION\" }`.",
            "oneOf": [
              { "type": "object", "required": ["format", "text"], "properties": { "format": { "const": "TEXT" }, "text": { "type": "string", "maxLength": 60 }, "example": { "type": "string" } } },
              { "type": "object", "required": ["format", "handle"], "properties": { "format": { "enum": ["IMAGE", "VIDEO", "DOCUMENT"] }, "handle": { "type": "string", "description": "De POST /instances/{instance_id}/templates/media." } } },
              { "type": "object", "required": ["format"], "properties": { "format": { "const": "LOCATION" } } }
            ]
          },
          "body": {
            "type": "object",
            "required": ["text"],
            "properties": {
              "text": { "type": "string", "maxLength": 1024 },
              "examples": { "type": "array", "items": { "type": "string" }, "description": "Um exemplo por variável, na ordem." }
            }
          },
          "footer": { "type": "string", "maxLength": 60, "description": "Sem variáveis." },
          "buttons": { "type": "array", "maxItems": 10, "items": { "$ref": "#/components/schemas/TemplateDraftButton" } },
          "cards": {
            "type": "array",
            "maxItems": 10,
            "description": "Carrossel. Exige `body` na mensagem; todos os cartões com o mesmo formato de mídia e a mesma quantidade de botões.",
            "items": {
              "type": "object",
              "required": ["header", "body"],
              "properties": {
                "header": { "type": "object", "required": ["format", "handle"], "properties": { "format": { "enum": ["IMAGE", "VIDEO", "DOCUMENT"] }, "handle": { "type": "string" } } },
                "body": { "type": "object", "required": ["text"], "properties": { "text": { "type": "string" }, "examples": { "type": "array", "items": { "type": "string" } } } },
                "buttons": { "type": "array", "items": { "$ref": "#/components/schemas/TemplateDraftButton" } }
              }
            }
          },
          "offer": { "type": "object", "description": "Oferta por tempo limitado (MARKETING).", "properties": { "text": { "type": "string" }, "hasExpiration": { "type": "boolean" } } },
          "auth": { "type": "object", "description": "Só AUTHENTICATION.", "properties": { "codeExpirationMinutes": { "type": "integer", "minimum": 1, "maximum": 90 }, "addSecurityRecommendation": { "type": "boolean" } } }
        }
      },
      "InteractiveButton": {
        "type": "object",
        "required": ["id", "title"],
        "properties": {
          "id": { "type": "string", "maxLength": 256, "description": "Volta no webhook em `reply.id` quando o contato toca. Decida por ele, não pelo título." },
          "title": { "type": "string", "maxLength": 20, "description": "Rótulo do botão. Precisa ser único na mensagem." }
        }
      },
      "InteractiveLink": {
        "type": "object",
        "required": ["display_text", "url"],
        "properties": {
          "display_text": { "type": "string", "maxLength": 20 },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "InteractiveHeader": {
        "type": "object",
        "required": ["type"],
        "description": "Opcional. Lista aceita só `text`. Mídia vai por `url` pública ou `id` de mídia já enviada à Meta.",
        "properties": {
          "type": { "type": "string", "enum": ["text", "image", "video", "document"] },
          "text": { "type": "string", "maxLength": 60, "description": "Quando `type` é `text`." },
          "url": { "type": "string", "format": "uri" },
          "id": { "type": "string" },
          "filename": { "type": "string", "description": "Só para `document`." }
        }
      },
      "InteractiveCard": {
        "type": "object",
        "required": ["header"],
        "description": "Cartão do carrossel. Leva `buttons` (respostas rápidas) OU `cta` (um botão de link) — e todos os cartões precisam do mesmo tipo e da mesma quantidade de botões.",
        "properties": {
          "header": {
            "type": "object",
            "required": ["type"],
            "properties": {
              "type": { "type": "string", "enum": ["image", "video"] },
              "url": { "type": "string", "format": "uri" },
              "id": { "type": "string" }
            }
          },
          "body": { "type": "string", "maxLength": 160, "description": "Opcional; até 2 quebras de linha." },
          "buttons": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/InteractiveButton" } },
          "cta": { "$ref": "#/components/schemas/InteractiveLink" }
        }
      },
      "Interactive": {
        "type": "object",
        "required": ["type", "body"],
        "description": "Mensagem interativa livre — só dentro da janela de 24 h. Forma mais curta que a da Meta (`url` em vez de `{ image: { link } }`, `buttons: [{ id, title }]` em vez de `{ type: \"reply\", reply }`); a montagem do payload oficial é feita pela FluxoAPI.\n\n| `type` | Campos | Limites |\n|---|---|---|\n| `button` | `buttons`, `header?`, `footer?` | 1 a 3 botões; `body` até 1.024 |\n| `list` | `list`, `header?` (só texto), `footer?` | até 10 seções e 10 linhas no total; `body` até 4.096 |\n| `cta_url` | `cta`, `header?`, `footer?` | `body` até 1.024 |\n| `carousel` | `cards` | 2 a 10 cartões; sem `header` nem `footer`; `body` até 1.024 |\n\nCampo de outro tipo é recusado (`buttons` num `list`, por exemplo).",
        "properties": {
          "type": { "type": "string", "enum": ["button", "list", "cta_url", "carousel"] },
          "header": { "$ref": "#/components/schemas/InteractiveHeader" },
          "body": { "type": "string", "minLength": 1, "maxLength": 4096 },
          "footer": { "type": "string", "maxLength": 60 },
          "buttons": { "type": "array", "minItems": 1, "maxItems": 3, "items": { "$ref": "#/components/schemas/InteractiveButton" } },
          "list": {
            "type": "object",
            "required": ["button", "sections"],
            "properties": {
              "button": { "type": "string", "maxLength": 20, "description": "Rótulo do botão que abre a lista." },
              "sections": {
                "type": "array",
                "minItems": 1,
                "maxItems": 10,
                "items": {
                  "type": "object",
                  "required": ["title", "rows"],
                  "properties": {
                    "title": { "type": "string", "maxLength": 24 },
                    "rows": {
                      "type": "array",
                      "minItems": 1,
                      "maxItems": 10,
                      "items": {
                        "type": "object",
                        "required": ["id", "title"],
                        "properties": {
                          "id": { "type": "string", "maxLength": 200, "description": "Único entre todas as seções." },
                          "title": { "type": "string", "maxLength": 24 },
                          "description": { "type": "string", "maxLength": 72 }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "cta": { "$ref": "#/components/schemas/InteractiveLink" },
          "cards": { "type": "array", "minItems": 2, "maxItems": 10, "items": { "$ref": "#/components/schemas/InteractiveCard" } }
        }
      },
      "InteractiveReply": {
        "type": "object",
        "description": "O que o contato escolheu. Vem em `data.reply` do webhook `message.received`.",
        "properties": {
          "kind": { "type": "string", "enum": ["button_reply", "list_reply", "template_button"], "description": "`button_reply`: botão de resposta ou resposta rápida do carrossel. `list_reply`: linha da lista. `template_button`: resposta rápida de modelo (aí `id` é o payload do botão)." },
          "id": { "type": "string" },
          "title": { "type": "string" },
          "description": { "type": "string", "description": "Só em `list_reply`, quando a linha tem descrição." }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Formato único de erro. Trate o `code`; mostre a `message`; guarde o `request_id` para o suporte.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_auth_headers",
                  "stale_timestamp",
                  "invalid_nonce",
                  "invalid_key",
                  "nonce_reused",
                  "invalid_signature",
                  "too_many_failures",
                  "rate_limited",
                  "insufficient_scope",
                  "not_found",
                  "invalid_input",
                  "window_closed",
                  "payment_required",
                  "instance_reconnecting",
                  "meta_error",
                  "template_not_found",
                  "range_too_wide",
                  "instance_required",
                  "instance_not_connected",
                  "not_readable",
                  "unknown_stage",
                  "reason_required",
                  "stage_mismatch",
                  "server_error"
                ],
                "description": "Identificador estável. É por ele que se programa."
              },
              "message": {
                "type": "string",
                "description": "Frase em português, pronta para mostrar a uma pessoa."
              },
              "request_id": {
                "type": "string",
                "example": "req_a1b2c3d4e5f6a7b8"
              },
              "required_scope": {
                "type": "string",
                "description": "Só em `insufficient_scope`."
              },
              "window_expired_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Só em `window_closed`: quando a janela fechou."
              },
              "hint": {
                "type": "string",
                "description": "Caminho sugerido para contornar o erro."
              },
              "meta_code": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Só em `meta_error`: o código que a Meta devolveu."
              },
              "current_stage": {
                "type": "string",
                "description": "Só em `stage_mismatch`."
              },
              "quota": {
                "type": "integer"
              },
              "usados": {
                "type": "integer"
              }
            }
          }
        },
        "example": {
          "error": {
            "code": "window_closed",
            "message": "O contato não escreve há mais de 24 horas. Use um modelo aprovado.",
            "request_id": "req_a1b2c3d4e5f6a7b8",
            "window_expired_at": "2026-09-03T11:27:00.000Z",
            "hint": "POST /v1/instances/inst_01J.../messages/template"
          }
        }
      },
      "Instance": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "inst_01JB8XR4KP2M9TVQ"
            ]
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "connected",
              "disconnected",
              "suspended"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "coexistence",
              "api_only"
            ]
          },
          "quality": {
            "type": "string",
            "enum": [
              "green",
              "yellow",
              "red",
              "unknown"
            ]
          },
          "throughput_mps": {
            "type": "integer"
          },
          "messaging_limit_24h": {
            "type": "integer"
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "conversation_id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "type": {
            "type": "string"
          },
          "text": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "read",
              "failed"
            ]
          },
          "sent_from_phone": {
            "type": "boolean",
            "description": "true quando o dono respondeu pelo aplicativo do celular."
          },
          "wamid": {
            "type": [
              "string",
              "null"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "media": {
            "$ref": "#/components/schemas/Media"
          }
        }
      },
      "Media": {
        "type": ["object", "null"],
        "description": "Arquivo da mensagem; `null` em texto. Baixe os bytes com um GET assinado em `download_path`. A Meta apaga a mídia 30 dias depois: guarde o arquivo se precisar dele além disso.",
        "properties": {
          "id": { "type": "string", "description": "Identificador da mídia na Meta. Útil para deduplicar." },
          "mime_type": { "type": ["string", "null"], "examples": ["image/jpeg", "audio/ogg; codecs=opus", "application/pdf"] },
          "filename": { "type": ["string", "null"], "description": "Nome do arquivo, em documento." },
          "caption": { "type": ["string", "null"], "description": "Legenda, quando houver." },
          "sha256": { "type": ["string", "null"], "description": "Hash informado pela Meta, para conferir o download." },
          "voice": { "type": "boolean", "description": "true em mensagem de voz gravada no app." },
          "link": { "type": ["string", "null"], "description": "Em mídia enviada por você: o `media.url` usado no envio." },
          "download_path": { "type": "string", "examples": ["/v1/messages/msg_01JB8YF2K9QX4TMA/media"], "description": "Rota para baixar o arquivo. Some à base da API e assine como qualquer GET." }
        }
      },
      "Template": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "parameterFormat": {
            "type": "string",
            "enum": [
              "NAMED",
              "POSITIONAL"
            ]
          },
          "headerFormat": {
            "type": [
              "string",
              "null"
            ],
            "description": "TEXT, IMAGE, VIDEO, DOCUMENT, LOCATION ou null."
          },
          "headerText": {
            "type": [
              "string",
              "null"
            ]
          },
          "bodyText": {
            "type": [
              "string",
              "null"
            ]
          },
          "footerText": {
            "type": [
              "string",
              "null"
            ]
          },
          "buttons": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "cards": {
            "type": "integer",
            "description": "Quantidade de cartões, quando o modelo é carrossel."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Um valor que precisa ser preenchido para enviar este modelo.",
              "properties": {
                "slot": {
                  "type": "string",
                  "enum": [
                    "header",
                    "body",
                    "button",
                    "card_header",
                    "card_body",
                    "card_button",
                    "offer"
                  ]
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "text",
                    "image",
                    "video",
                    "document",
                    "location",
                    "url_suffix",
                    "coupon_code",
                    "flow_token",
                    "otp_code",
                    "expiration_ms"
                  ]
                },
                "ref": {
                  "type": "string",
                  "description": "Nome da variável (formato nomeado), número da posição (formato posicional) ou índice do botão."
                },
                "label": {
                  "type": "string"
                },
                "example": {
                  "type": "string"
                },
                "buttonIndex": {
                  "type": "integer"
                },
                "cardIndex": {
                  "type": "integer"
                },
                "required": {
                  "type": "boolean"
                }
              }
            }
          },
          "sendable": {
            "type": "boolean"
          },
          "blockedReason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Conversation": {
        "type": "object",
        "description": "Um fio por contato dentro de uma instância.",
        "properties": {
          "id": {
            "type": "string",
            "example": "conv_01M1HQ7SE7C7QFBGMN8Y8ZNKAP"
          },
          "instance_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "closed"
            ],
            "description": "Reabre sozinha a cada mensagem recebida."
          },
          "unread": {
            "type": "integer",
            "description": "Recebidas sem leitura. Zera ao abrir a conversa no painel ou ao marcar a mensagem como lida."
          },
          "window_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Fim da janela de 24 h. `null` ou no passado: só modelo aprovado."
          },
          "last_message_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_message_text": {
            "type": "string",
            "description": "Prévia, cortada em 500 caracteres."
          },
          "phone": {
            "type": "string",
            "example": "+5511999999999"
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nome do perfil no WhatsApp, quando o contato o expõe."
          },
          "lead_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "stage_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "value_brl": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "Lead": {
        "type": "object",
        "description": "Oportunidade ligada a um contato e, quase sempre, a uma conversa.",
        "properties": {
          "id": {
            "type": "string",
            "example": "lead_01M1HQ7SE7C7QFBGMN8Y8ZNKAP"
          },
          "stage_key": {
            "type": "string",
            "example": "proposta"
          },
          "value_brl": {
            "type": [
              "number",
              "null"
            ],
            "description": "Valor em reais."
          },
          "probability": {
            "type": [
              "number",
              "null"
            ],
            "description": "Entre 0 e 1, vem da etapa. Usada na previsão ponderada."
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "conversation_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "hours_in_stage": {
            "type": "number",
            "description": "Horas desde a entrada na etapa atual."
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": "string"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Paginacao": {
        "type": "object",
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Passe em `cursor` para a próxima página. `null` é fim. É carimbo de tempo codificado, não deslocamento: nada pula nem repete quando chegam mensagens durante a paginação."
          }
        }
      }
    },
    "responses": {
      "NaoAutenticado": {
        "description": "Cabeçalhos ausentes, chave inválida ou assinatura que não confere.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "assinatura": {
                "summary": "Assinatura não confere",
                "value": {
                  "error": {
                    "code": "invalid_signature",
                    "message": "A assinatura não confere. Verifique a string assinada: v1, timestamp, nonce, método, caminho e sha256 do corpo."
                  }
                }
              },
              "relogio": {
                "summary": "Relógio fora da janela",
                "value": {
                  "error": {
                    "code": "stale_timestamp",
                    "message": "O carimbo de tempo está fora da janela de 5 minutos. Sincronize o relógio do servidor por NTP."
                  }
                }
              }
            }
          }
        }
      },
      "SemPermissao": {
        "description": "A chave existe, mas não tem o escopo necessário.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "insufficient_scope",
                "message": "Esta chave não tem a permissão messages:send.",
                "required_scope": "messages:send"
              }
            }
          }
        }
      },
      "NaoEncontrado": {
        "description": "Recurso inexistente — ou fora do alcance desta chave. As duas situações respondem igual, de propósito: distinguir permitiria enumerar.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NonceReusado": {
        "description": "Nonce repetido dentro da janela de 10 minutos. Gere um novo por requisição.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "nonce_reused",
                "message": "Este nonce já foi usado. Gere um novo a cada requisição."
              }
            }
          }
        }
      },
      "PagamentoNecessario": {
        "description": "Mensalidade em aberto além da tolerância. **Só o envio para** — receber, ler e consultar continuam funcionando.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "payment_required",
                "message": "A mensalidade desta instância está em aberto. Regularize em Cobrança."
              }
            }
          }
        }
      },
      "LimiteExcedido": {
        "description": "Limite do plano estourado. Respeite o `Retry-After`.",
        "headers": {
          "Retry-After": {
            "description": "Segundos a esperar.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix em segundos.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Você atingiu o limite do seu plano. Respeite o Retry-After."
              }
            }
          }
        }
      },
      "DadosInvalidos": {
        "description": "O corpo ou a query não passaram na validação.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "fluxoHmac": []
    }
  ],
  "paths": {
    "/instances": {
      "get": {
        "summary": "Listar instancias",
        "operationId": "listInstances",
        "responses": {
          "200": {
            "description": "Lista",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Instance"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Instâncias"
        ],
        "description": "Chave presa a uma instância enxerga só a dela — a lista vem com um item."
      }
    },
    "/instances/{instance_id}": {
      "get": {
        "summary": "Detalhar instancia",
        "operationId": "getInstance",
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instancia",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "404": {
            "description": "Nao encontrada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Instâncias"
        ],
        "description": "Traz também `conversations_24h` e `messages_24h`, contados na hora."
      }
    },
    "/instances/{instance_id}/messages": {
      "post": {
        "summary": "Enviar mensagem",
        "operationId": "sendMessage",
        "description": "Só dentro da janela de 24 h. Fora dela, use o envio por modelo.\n\nEnvia texto, mídia ou **mensagem interativa** (`type: \"interactive\"`): botões de resposta (até 3), lista (até 10 linhas em até 10 seções), botão de link (`cta_url`) e carrossel de mídia (2 a 10 cartões). O corpo interativo é validado aqui com as regras da Meta — erro de limite volta como `422 invalid_input` apontando o campo, em vez de falhar depois na fila. Quando o contato toca num botão ou linha, o webhook `message.received` chega com `type: \"interactive\"` e `reply: { kind, id, title }`.\n\nCarrossel **fora** da janela continua sendo modelo: use `/messages/template` com `cards`.\n\nResponde **202**: a mensagem entra na fila e é entregue respeitando a vazão do número. Acompanhe por `message.status` ou por `GET /messages`.\n\n**Escopo exigido:** `messages:send`.",
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "examples": [
                      "+5519991802264"
                    ]
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "text",
                      "image",
                      "document",
                      "audio",
                      "video",
                      "interactive"
                    ],
                    "default": "text"
                  },
                  "text": {
                    "type": "object",
                    "properties": {
                      "body": {
                        "type": "string",
                        "maxLength": 4096
                      }
                    }
                  },
                  "media": {
                    "type": "object",
                    "properties": {
                      "url": {
                        "type": "string",
                        "format": "uri",
                        "description": "URL pública HTTPS. Quem baixa é a Meta, no envio. Limites: imagem 5 MB, áudio e vídeo 16 MB, documento 100 MB."
                      },
                      "caption": {
                        "type": "string",
                        "maxLength": 1024,
                        "description": "Legenda. Ignorada em áudio — a Meta não aceita."
                      },
                      "filename": {
                        "type": "string",
                        "maxLength": 240,
                        "description": "Só em `document`: o nome que aparece para o contato."
                      }
                    }
                  },
                  "interactive": {
                    "$ref": "#/components/schemas/Interactive"
                  },
                  "reply_to": {
                    "type": "string"
                  },
                  "idempotency_key": {
                    "type": "string",
                    "description": "A mesma chave devolve a mesma mensagem por 7 dias, sem reenviar."
                  }
                }
              },
              "examples": {
                "texto": {
                  "summary": "Texto",
                  "value": { "to": "+5519991802264", "type": "text", "text": { "body": "Oi! Sua reserva está confirmada." } }
                },
                "botoes": {
                  "summary": "Botões de resposta",
                  "value": {
                    "to": "+5519991802264",
                    "type": "interactive",
                    "interactive": {
                      "type": "button",
                      "header": { "type": "image", "url": "https://exemplo.com/visita.jpg" },
                      "body": "Confirma a visita amanhã às 9h?",
                      "footer": "Responda por um botão",
                      "buttons": [
                        { "id": "confirmar", "title": "Confirmar" },
                        { "id": "remarcar", "title": "Remarcar" }
                      ]
                    }
                  }
                },
                "lista": {
                  "summary": "Lista",
                  "value": {
                    "to": "+5519991802264",
                    "type": "interactive",
                    "interactive": {
                      "type": "list",
                      "header": { "type": "text", "text": "Horários" },
                      "body": "Escolha um horário para a visita:",
                      "footer": "Imobiliária Exemplo",
                      "list": {
                        "button": "Ver horários",
                        "sections": [
                          { "title": "Manhã", "rows": [
                            { "id": "h09", "title": "09:00", "description": "Com o corretor Paulo" },
                            { "id": "h11", "title": "11:00" }
                          ] },
                          { "title": "Tarde", "rows": [
                            { "id": "h14", "title": "14:00" }
                          ] }
                        ]
                      }
                    }
                  }
                },
                "link": {
                  "summary": "Botão de link (cta_url)",
                  "value": {
                    "to": "+5519991802264",
                    "type": "interactive",
                    "interactive": {
                      "type": "cta_url",
                      "body": "Seu boleto está pronto.",
                      "cta": { "display_text": "Abrir boleto", "url": "https://exemplo.com/b/123" }
                    }
                  }
                },
                "carrossel": {
                  "summary": "Carrossel de mídia",
                  "value": {
                    "to": "+5519991802264",
                    "type": "interactive",
                    "interactive": {
                      "type": "carousel",
                      "body": "Separei três opções abaixo de R$ 25:",
                      "cards": [
                        {
                          "header": { "type": "image", "url": "https://exemplo.com/echeveria.jpg" },
                          "body": "*Echeveria azul*\nSuculenta de folhas azuladas.",
                          "cta": { "display_text": "Comprar", "url": "https://loja.exemplo.com/echeveria" }
                        },
                        {
                          "header": { "type": "image", "url": "https://exemplo.com/haworthia.jpg" },
                          "body": "*Haworthia zebra*\nListras brancas, visual moderno.",
                          "cta": { "display_text": "Comprar", "url": "https://loja.exemplo.com/haworthia" }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aceita para envio",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "422": {
            "description": "Janela de 24 h fechada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Instancia reconectando"
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "404": {
            "$ref": "#/components/responses/NaoEncontrado"
          },
          "402": {
            "$ref": "#/components/responses/PagamentoNecessario"
          }
        },
        "tags": [
          "Mensagens"
        ]
      }
    },
    "/instances/{instance_id}/messages/template": {
      "post": {
        "summary": "Enviar modelo aprovado",
        "operationId": "sendTemplate",
        "description": "Unico caminho fora da janela de 24 h. Sempre abre conversa cobrada.\n\nO único caminho fora da janela de 24 h, e **sempre cobrado** pela Meta — a resposta traz `billable: true`. Os componentes são montados a partir da definição real do modelo, não do que você mandou: faltar o parâmetro de um botão faz a Meta recusar a mensagem inteira.\n\n**Escopo exigido:** `messages:send`.",
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "template"
                ],
                "properties": {
                  "to": {
                    "type": "string"
                  },
                  "template": {
                    "type": "object",
                    "required": [
                      "name",
                      "language"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "language": {
                        "type": "string",
                        "examples": [
                          "pt_BR"
                        ]
                      },
                      "variables": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Forma antiga, mantida: variáveis do CORPO por posição, na ordem numérica. Equivale a `body`."
                      },
                      "header": {
                        "type": "object",
                        "description": "Valores do cabeçalho.",
                        "properties": {
                          "text": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Variáveis do cabeçalho de texto."
                          },
                          "media": {
                            "type": "object",
                            "description": "Arquivo do cabeçalho. Use link (URL pública) ou id (mídia já enviada à Meta).",
                            "properties": {
                              "link": {
                                "type": "string",
                                "format": "uri"
                              },
                              "id": {
                                "type": "string"
                              },
                              "filename": {
                                "type": "string"
                              }
                            }
                          },
                          "location": {
                            "type": "object",
                            "properties": {
                              "latitude": {
                                "type": "string"
                              },
                              "longitude": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "address": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      },
                      "body": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Variáveis do corpo. Chave = posição (\"1\") ou nome (\"first_name\"), conforme o parameterFormat do modelo."
                      },
                      "buttons": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Chave = índice do botão (zero-based, contando TODOS os botões). Valor = trecho da URL, código do cupom ou token do fluxo."
                      },
                      "code": {
                        "type": "string",
                        "maxLength": 15,
                        "description": "Modelos de autenticação. O mesmo código vai no corpo e no botão — isso é feito aqui, não repita."
                      },
                      "offer_expiration_ms": {
                        "type": "integer",
                        "description": "Fim da oferta por tempo limitado, em milissegundos."
                      },
                      "cards": {
                        "type": "object",
                        "description": "Carrossel. Chave = índice do cartão (\"0\", \"1\", …).",
                        "additionalProperties": {
                          "type": "object",
                          "properties": {
                            "header": {
                              "type": "object",
                              "description": "Arquivo do cabeçalho. Use link (URL pública) ou id (mídia já enviada à Meta).",
                              "properties": {
                                "link": {
                                  "type": "string",
                                  "format": "uri"
                                },
                                "id": {
                                  "type": "string"
                                },
                                "filename": {
                                  "type": "string"
                                }
                              }
                            },
                            "body": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            "buttons": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      },
                      "components": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        },
                        "description": "Escotilha: o array `components` da Meta, pronto. Pula a validação e vai como veio. Existe para não travar recurso novo da Meta atrás de um deploy nosso."
                      }
                    },
                    "description": "O modelo INTEIRO. Cada tipo de componente tem uma forma diferente no payload da Meta, e faltar um faz a mensagem toda ser recusada — por isso os valores enviados aqui são conferidos contra a definição real do modelo antes de sair."
                  },
                  "idempotency_key": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aceita. `billable: true` e `preview` com o texto que o contato vai ver.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "404": {
            "description": "Modelo inexistente nesta conta, no idioma pedido."
          },
          "422": {
            "description": "Falta um parâmetro do modelo, ou ele não está APPROVED. A mensagem diz qual."
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "402": {
            "$ref": "#/components/responses/PagamentoNecessario"
          }
        },
        "tags": [
          "Mensagens"
        ]
      }
    },
    "/messages": {
      "get": {
        "summary": "Listar mensagens",
        "operationId": "listMessages",
        "description": "Janela maxima de 90 dias por consulta.\n\nJanela máxima de **90 dias por consulta**. Sem `since`, usa os últimos 7 dias.\n\n**Escopo exigido:** `messages:read`.",
        "parameters": [
          {
            "name": "conversation_id",
            "in": "query",
            "description": "Restringe a uma conversa.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instance_id",
            "in": "query",
            "description": "Restringe a um número.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Sentido da mensagem.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "inbound",
                "outbound"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Estado atual.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "sent",
                "delivered",
                "read",
                "failed"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Início da janela (ISO 8601). Padrão: 7 dias atrás.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Fim da janela (ISO 8601). Padrão: agora.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Máximo de 200.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Mensagens"
        ]
      }
    },
    "/conversations": {
      "get": {
        "summary": "Listar conversas",
        "operationId": "listConversations",
        "parameters": [
          {
            "name": "instance_id",
            "in": "query",
            "description": "Restringe a um número.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Uma ou mais, separadas por vírgula: `open`, `closed`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Só conversas com mensagem depois deste instante (ISO 8601).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "O `next_cursor` da página anterior.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Máximo de 100.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Conversas"
        ],
        "description": "Ordenado pela última mensagem, mais recente primeiro. Pagina por cursor.\n\n**Escopo exigido:** `conversations:read`."
      }
    },
    "/conversations/{conversation_id}/messages": {
      "get": {
        "summary": "Historico da conversa",
        "operationId": "getConversationMessages",
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mensagens e window_expires_at",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "404": {
            "$ref": "#/components/responses/NaoEncontrado"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Conversas"
        ],
        "description": "O fio completo. `window_expires_at` no topo diz se ainda dá para responder livremente.\n\n**Escopo exigido:** `messages:read`."
      }
    },
    "/leads": {
      "get": {
        "summary": "Listar oportunidades",
        "operationId": "listLeads",
        "responses": {
          "200": {
            "description": "Lista",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Funil"
        ],
        "description": "Traz um `summary` com contagem, valor em aberto e previsão ponderada pela probabilidade de cada etapa.\n\n**Escopo exigido:** `funnel:read`.",
        "parameters": [
          {
            "name": "stage",
            "in": "query",
            "description": "Uma ou mais etapas, separadas por vírgula. Ex.: `novo,proposta`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "min_value",
            "in": "query",
            "description": "Valor mínimo em reais.",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "stale_hours",
            "in": "query",
            "description": "Só o que está parado há mais horas que isto.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Máximo de 200.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ]
      }
    },
    "/leads/{lead_id}/stage": {
      "post": {
        "summary": "Mover etapa",
        "operationId": "moveLeadStage",
        "parameters": [
          {
            "name": "lead_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "stage"
                ],
                "properties": {
                  "stage": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "expected_stage": {
                    "type": "string",
                    "description": "Trava otimista: se alguem moveu antes, devolve 409."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Movida",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "409": {
            "description": "stage_mismatch"
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "404": {
            "$ref": "#/components/responses/NaoEncontrado"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Funil"
        ],
        "description": "Mande `expected_stage` para travar concorrência: duas mensagens do mesmo contato chegando juntas avançariam o mesmo lead duas etapas. Divergindo, responde `409 stage_mismatch` com `current_stage`.\n\n**Escopo exigido:** `funnel:write`."
      }
    },
    "/instances/{instance_id}/templates": {
      "get": {
        "summary": "Listar modelos aprovados",
        "operationId": "listTemplates",
        "description": "Modelos aprovados do WABA a que este número pertence.\n\nO modelo pertence à conta WhatsApp Business (WABA), não ao número: dois números do mesmo WABA devolvem a mesma lista.\n\nCada item traz `fields` — exatamente o que precisa ser preenchido para enviar, já com o exemplo aprovado. É o que evita descobrir na Meta, em forma de código numérico, que faltava o parâmetro do botão de URL.\n\nO modelo pertence ao WABA, não ao número. A resposta traz `fields`: exatamente o que precisa ser preenchido para enviar, já com exemplo.\n\n**Escopo exigido:** `templates:read ou messages:send`.",
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filtra pelo estado na Meta. Ex.: `APPROVED`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Ex.: `MARKETING`, `UTILITY`, `AUTHENTICATION`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "Código exato. Ex.: `pt_BR`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Busca por parte do nome.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refresh",
            "in": "query",
            "description": "Ignora o cache e relê da Meta. Consome orçamento da Graph API.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catálogo do WABA deste número.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    },
                    "waba_id": {
                      "type": "string"
                    },
                    "cached": {
                      "type": "boolean",
                      "description": "true quando veio do cache de 5 minutos."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "409": {
            "description": "A instância ainda não concluiu a conexão com a Meta."
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "404": {
            "$ref": "#/components/responses/NaoEncontrado"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Modelos"
        ]
      },
      "post": {
        "summary": "Criar modelo",
        "operationId": "createTemplate",
        "description": "Submete um modelo novo à análise da Meta, no WABA deste número. Nasce `PENDING`; a Meta aprova, recusa ou recategoriza em minutos a horas — acompanhe por `GET /instances/{instance_id}/templates?name=…` ou pela aba Modelos do painel.\n\nVocê manda a **receita**, não os `components` da Meta. Ela passa pelo mesmo validador do painel antes de sair: a Meta recusa criação com `(#100) Invalid parameter` sem dizer qual, e aqui o erro volta em português, com o campo.\n\nRegras que mais derrubam:\n\n- `name` aceita letras minúsculas, números e `_` — sem espaço e sem acento (maiúsculas são convertidas).\n- Variáveis sequenciais (`{{1}}`, `{{2}}`…), cada uma com exemplo em `body.examples` — sem exemplo a Meta recusa.\n- Variável em botão de URL só no **fim** da URL, com `urlExample`.\n- `AUTHENTICATION` não tem corpo escrito: o texto é fixo da Meta; configure em `auth`. Sem `buttons`, entra um botão `OTP` de copiar código.\n- Cabeçalho de mídia leva `handle`, não link: suba o arquivo antes em `POST /instances/{instance_id}/templates/media`.\n- Carrossel: até 10 cartões, todos com o mesmo tipo de mídia e a mesma quantidade de botões.\n\nCom chave `test`, valida tudo e responde `202` com `enviaria_para_a_meta` — nada é submetido.\n\nNo MCP, `validate_template` confere o mesmo rascunho sem criar.\n\n**Escopo exigido:** `templates:write` ou `messages:send`.",
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateDraft"
              },
              "examples": {
                "utilidade": {
                  "summary": "Utilidade com variáveis e botão de URL",
                  "value": {
                    "name": "pedido_enviado",
                    "language": "pt_BR",
                    "category": "UTILITY",
                    "header": { "format": "TEXT", "text": "Pedido {{1}}", "example": "A-4471" },
                    "body": { "text": "Olá {{1}}, seu pedido saiu para entrega e chega até {{2}}.", "examples": ["Marina", "sexta-feira"] },
                    "footer": "Loja Exemplo",
                    "buttons": [
                      { "type": "URL", "text": "Rastrear", "url": "https://loja.exemplo.com/r/{{1}}", "urlExample": "A-4471" },
                      { "type": "QUICK_REPLY", "text": "Falar com atendente" }
                    ]
                  }
                },
                "marketingImagem": {
                  "summary": "Marketing com imagem no cabeçalho",
                  "value": {
                    "name": "promo_setembro",
                    "language": "pt_BR",
                    "category": "MARKETING",
                    "header": { "format": "IMAGE", "handle": "4::aW1hZ2UvanBlZw==:ARb…" },
                    "body": { "text": "{{1}}, 20% em toda a coleção até domingo.", "examples": ["Marina"] },
                    "buttons": [{ "type": "URL", "text": "Ver coleção", "url": "https://loja.exemplo.com/setembro" }]
                  }
                },
                "autenticacao": {
                  "summary": "Autenticação com código",
                  "value": {
                    "name": "codigo_acesso",
                    "language": "pt_BR",
                    "category": "AUTHENTICATION",
                    "auth": { "codeExpirationMinutes": 10, "addSecurityRecommendation": true },
                    "buttons": [{ "type": "OTP", "otp_type": "COPY_CODE", "text": "Copiar código" }]
                  }
                },
                "carrossel": {
                  "summary": "Carrossel de dois cartões",
                  "value": {
                    "name": "vitrine_semana",
                    "language": "pt_BR",
                    "category": "MARKETING",
                    "body": { "text": "Separamos estes para você, {{1}}:", "examples": ["Marina"] },
                    "cards": [
                      {
                        "header": { "format": "IMAGE", "handle": "4::aW1h…:ARb1" },
                        "body": { "text": "Tênis leve, R$ 199" },
                        "buttons": [{ "type": "URL", "text": "Comprar", "url": "https://loja.exemplo.com/tenis" }]
                      },
                      {
                        "header": { "format": "IMAGE", "handle": "4::aW1h…:ARb2" },
                        "body": { "text": "Mochila, R$ 149" },
                        "buttons": [{ "type": "URL", "text": "Comprar", "url": "https://loja.exemplo.com/mochila" }]
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Submetido. Ainda em análise na Meta.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "description": "Id do modelo na Meta." },
                        "name": { "type": "string" },
                        "language": { "type": "string" },
                        "status": { "type": "string", "examples": ["PENDING"] },
                        "category": { "type": "string", "description": "A Meta pode recategorizar; confira depois." }
                      }
                    }
                  }
                },
                "example": { "data": { "id": "1284719950312245", "name": "pedido_enviado", "language": "pt_BR", "status": "PENDING", "category": "UTILITY" } }
              }
            }
          },
          "202": {
            "description": "Chave `test`: validado e não submetido. Traz `simulated: true` e `enviaria_para_a_meta`."
          },
          "409": {
            "description": "A instância ainda não concluiu a conexão com a Meta."
          },
          "422": {
            "description": "Receita inválida (`invalid_input` com `field`, ou código do validador, como `exemplo_faltando`, `nome_invalido`, `url_variavel_no_meio`, `cartoes_diferentes`) ou recusada pela Meta (`meta_rejected`, com `meta_code`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "404": {
            "$ref": "#/components/responses/NaoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          }
        },
        "tags": [
          "Modelos"
        ]
      }
    },
    "/instances/{instance_id}/templates/media": {
      "post": {
        "summary": "Subir mídia de cabeçalho de modelo",
        "operationId": "uploadTemplateMedia",
        "description": "Devolve o `handle` que vai em `header.handle` (ou `cards[].header.handle`) ao criar modelo com imagem, vídeo ou documento.\n\nAo **enviar** um modelo, o cabeçalho aceita link. Ao **criar**, a Meta exige este handle, que só sai da Resumable Upload API do app — por isso a rota existe.\n\nO arquivo vai em **base64** dentro do JSON, até **5 MB** de arquivo (o corpo aceita até 8 MB). Não há busca por URL, de propósito.\n\nCom chave `test`, devolve um handle `SIMULADO_…` que só serve para criação com chave `test`.\n\n**Escopo exigido:** `templates:write` ou `messages:send`.",
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["mime", "data"],
                "properties": {
                  "mime": { "type": "string", "examples": ["image/jpeg", "image/png", "video/mp4", "application/pdf"] },
                  "data": { "type": "string", "contentEncoding": "base64", "description": "O arquivo inteiro em base64." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Handle pronto para usar na criação.",
            "content": {
              "application/json": {
                "example": { "data": { "handle": "4::aW1hZ2UvanBlZw==:ARb…", "bytes": 184233 } }
              }
            }
          },
          "422": {
            "description": "`invalid_input`, `arquivo_vazio`, `arquivo_grande` (mais de 5 MB) ou `upload_recusado` pela Meta.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "404": {
            "$ref": "#/components/responses/NaoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          }
        },
        "tags": [
          "Modelos"
        ]
      }
    },
    "/templates": {
      "get": {
        "summary": "Listar modelos aprovados (instância na query)",
        "operationId": "listTemplatesByQuery",
        "description": "O mesmo catálogo de /instances/{instance_id}/templates, mais confortável para chaves presas a um número só.\n\nO mesmo catálogo, com a instância na query — mais confortável para quem usa uma chave presa a um número só.\n\n**Escopo exigido:** `templates:read ou messages:send`.",
        "parameters": [
          {
            "name": "instance_id",
            "in": "query",
            "description": "Obrigatório quando a chave não está presa a uma instância.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filtra pelo estado na Meta. Ex.: `APPROVED`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Ex.: `MARKETING`, `UTILITY`, `AUTHENTICATION`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "Código exato. Ex.: `pt_BR`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Busca por parte do nome.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refresh",
            "in": "query",
            "description": "Ignora o cache e relê da Meta. Consome orçamento da Graph API.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catálogo do WABA deste número.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    },
                    "waba_id": {
                      "type": "string"
                    },
                    "cached": {
                      "type": "boolean",
                      "description": "true quando veio do cache de 5 minutos."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "409": {
            "description": "A instância ainda não concluiu a conexão com a Meta."
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "422": {
            "$ref": "#/components/responses/DadosInvalidos"
          }
        },
        "tags": [
          "Modelos"
        ]
      }
    },
    "/messages/{message_id}/media": {
      "get": {
        "summary": "Baixar a mídia de uma mensagem",
        "operationId": "getMessageMedia",
        "description": "Devolve os **bytes** do arquivo (foto, áudio, vídeo, documento, figurinha), com `Content-Type` e `Content-Disposition` certos — não é JSON.\n\nA Meta não entrega o arquivo no webhook, só um identificador, e o endereço que ele gera expira em 5 minutos e exige o token do número. Esta rota faz esse caminho por você. O `message.received` já traz o caminho pronto em `data.media.download_path`.\n\nAssinatura: igual a qualquer GET, com corpo vazio (`sha256` da string vazia).\n\nA Meta apaga a mídia **30 dias** depois do envio; a partir daí a resposta é `410`. Se precisar do arquivo por mais tempo, baixe ao receber o evento.\n\n**Escopo exigido:** `messages:read`.",
        "parameters": [
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "in": "query",
            "required": false,
            "description": "`1` força `Content-Disposition: attachment`.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "O arquivo.",
            "headers": {
              "Content-Type": {
                "description": "Tipo real do arquivo. HTML, SVG e outros tipos executáveis saem como `application/octet-stream`.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "description": "`inline` ou `attachment`, com o nome do arquivo.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Media-Mime-Type": {
                "description": "Tipo informado pela Meta, sempre — mesmo quando o download sai genérico.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "audio/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "video/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "Mídia enviada por você com link: redireciona para o seu próprio `media.url`."
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "404": {
            "description": "Mensagem não encontrada (`not_found`) ou sem arquivo (`no_media`)."
          },
          "410": {
            "description": "`media_expired` — a Meta já apagou a mídia (30 dias)."
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          },
          "502": {
            "description": "`media_unavailable` — a Meta recusou o download. Tente de novo."
          },
          "503": {
            "description": "`instance_reconnecting` — o número está desconectado."
          }
        },
        "tags": [
          "Mensagens"
        ]
      }
    },
    "/messages/{message_id}/read": {
      "post": {
        "summary": "Marcar mensagem como lida",
        "operationId": "markMessageRead",
        "description": "Os dois traços azuis no celular do contato. Só vale para mensagem recebida (`direction: inbound`) que já tenha `wamid`. Zera também o contador de não lidas da conversa.\n\nOs dois traços azuis no celular do contato. Vai direto à Meta, sem fila: recibo atrasado não serve, e não deve disputar vazão com mensagem de verdade. Só vale para mensagem **recebida** que tenha `wamid`.\n\n**Escopo exigido:** `messages:send`.",
        "parameters": [
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Marcada.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Teto do plano por minuto.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Quanto sobra na janela.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix em segundos.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "422": {
            "description": "A mensagem é de saída, ou ainda não tem wamid."
          },
          "404": {
            "description": "Mensagem não encontrada."
          },
          "401": {
            "$ref": "#/components/responses/NaoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SemPermissao"
          },
          "409": {
            "$ref": "#/components/responses/NonceReusado"
          },
          "429": {
            "$ref": "#/components/responses/LimiteExcedido"
          }
        },
        "tags": [
          "Mensagens"
        ]
      }
    },
    "/health": {
      "servers": [
        {
          "url": "https://api.fluxoapi.com.br",
          "description": "Raiz da API"
        }
      ],
      "get": {
        "tags": [
          "Serviço"
        ],
        "summary": "Saúde da plataforma",
        "operationId": "health",
        "security": [],
        "description": "Sem autenticação — feito para o seu monitoramento. Responde **200** quando banco e Redis respondem, e **503** quando algum deles está fora. Um 503 aqui significa que envios estão parados; recebimento também.",
        "responses": {
          "200": {
            "description": "Tudo de pé.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok",
                  "db": true,
                  "redis": true,
                  "version": "1.0.0",
                  "at": "2026-09-04T12:00:00.000Z"
                }
              }
            }
          },
          "503": {
            "description": "Alguma dependência fora.",
            "content": {
              "application/json": {
                "example": {
                  "status": "degraded",
                  "db": true,
                  "redis": false,
                  "version": "1.0.0",
                  "at": "2026-09-04T12:00:00.000Z"
                }
              }
            }
          }
        }
      }
    },
    "/metrics": {
      "servers": [
        {
          "url": "https://api.fluxoapi.com.br",
          "description": "Raiz da API"
        }
      ],
      "get": {
        "tags": [
          "Serviço"
        ],
        "summary": "Métricas no formato Prometheus",
        "operationId": "metrics",
        "security": [],
        "description": "Exige `Authorization: Bearer <METRICS_TOKEN>`. Sem token configurado, a rota responde **404** em produção — as métricas contam mensagens por conta, falhas de assinatura e profundidade de fila, ou seja, o mapa da operação.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "Bearer 3f9c…"
          }
        ],
        "responses": {
          "200": {
            "description": "Texto no formato de exposição do Prometheus.",
            "content": {
              "text/plain": {
                "example": "fluxo_messages_outbound 128\nfluxo_dlq_depth 0\n"
              }
            }
          },
          "404": {
            "description": "Token ausente, errado, ou não configurado no servidor.",
            "content": {
              "application/json": {}
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "servers": [
        {
          "url": "https://mcp.fluxoapi.com.br",
          "description": "Servidor MCP"
        }
      ],
      "get": {
        "tags": [
          "MCP"
        ],
        "summary": "Metadata do recurso protegido (RFC 9728)",
        "operationId": "prm",
        "security": [],
        "description": "É por aqui que o cliente MCP descobre qual Authorization Server usar. O 401 do servidor MCP aponta esta URL no cabeçalho `WWW-Authenticate`.",
        "responses": {
          "200": {
            "description": "Metadata.",
            "content": {
              "application/json": {
                "example": {
                  "resource": "https://mcp.fluxoapi.com.br",
                  "authorization_servers": [
                    "https://auth.fluxoapi.com.br"
                  ],
                  "scopes_supported": [
                    "instances:read",
                    "docs:read",
                    "logs:read"
                  ],
                  "bearer_methods_supported": [
                    "header"
                  ],
                  "resource_documentation": "https://fluxoapi.com.br/docs/"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "servers": [
        {
          "url": "https://auth.fluxoapi.com.br",
          "description": "Authorization Server"
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Metadata do Authorization Server (RFC 8414)",
        "operationId": "asm",
        "security": [],
        "description": "Diz onde autorizar, onde trocar o código, onde revogar e onde se registrar. Cliente que não encontra `registration_endpoint` aqui aborta com \"does not support dynamic client registration\".",
        "responses": {
          "200": {
            "description": "Metadata.",
            "content": {
              "application/json": {
                "example": {
                  "issuer": "https://auth.fluxoapi.com.br",
                  "authorization_endpoint": "https://auth.fluxoapi.com.br/oauth/authorize",
                  "token_endpoint": "https://auth.fluxoapi.com.br/oauth/token",
                  "revocation_endpoint": "https://auth.fluxoapi.com.br/oauth/revoke",
                  "registration_endpoint": "https://auth.fluxoapi.com.br/oauth/register",
                  "scopes_supported": [
                    "instances:read",
                    "docs:read",
                    "logs:read"
                  ],
                  "response_types_supported": [
                    "code"
                  ],
                  "grant_types_supported": [
                    "authorization_code",
                    "refresh_token"
                  ],
                  "code_challenge_methods_supported": [
                    "S256"
                  ],
                  "token_endpoint_auth_methods_supported": [
                    "none"
                  ],
                  "authorization_response_iss_parameter_supported": true
                }
              }
            }
          }
        }
      }
    },
    "/oauth/register": {
      "servers": [
        {
          "url": "https://auth.fluxoapi.com.br",
          "description": "Authorization Server"
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Registro dinâmico de cliente (RFC 7591)",
        "operationId": "oauthRegister",
        "security": [],
        "description": "Aberto por definição do protocolo: quem se registra ainda não tem conta. Registrar **não concede nada** — o cliente nasce sem dono, sem token e sem conta ligada. O acesso só existe depois que uma pessoa entra e aprova na tela de consentimento.\n\nLimite de 10 registros por hora por origem. Cliente que nunca vira token é apagado em 7 dias. `redirect_uris` aceita apenas `https://`, ou `http://` em `localhost`, `127.0.0.1` e `::1` — sem isso o registro viraria uma máquina de redirecionamento aberto. Só emitimos cliente **público** com PKCE.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "redirect_uris"
                ],
                "properties": {
                  "client_name": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Aparece na tela de consentimento, escapado e com aviso de que foi auto-declarado."
                  },
                  "redirect_uris": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "grant_types": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "authorization_code",
                        "refresh_token"
                      ]
                    }
                  },
                  "response_types": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "code"
                      ]
                    }
                  },
                  "token_endpoint_auth_method": {
                    "type": "string",
                    "enum": [
                      "none"
                    ]
                  },
                  "scope": {
                    "type": "string",
                    "description": "Separados por espaço. É um TETO: o que vale é o que a pessoa aprovar."
                  }
                }
              },
              "example": {
                "client_name": "Claude Desktop",
                "redirect_uris": [
                  "http://localhost:33418/callback"
                ],
                "grant_types": [
                  "authorization_code",
                  "refresh_token"
                ],
                "token_endpoint_auth_method": "none",
                "scope": "instances:read docs:read logs:read"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cliente criado. Não há `client_secret` — é cliente público.",
            "content": {
              "application/json": {
                "example": {
                  "client_id": "mcp_01M1P6VCX987SHE7R5M1YWH3K5",
                  "client_id_issued_at": 1788525197,
                  "client_name": "Claude Desktop",
                  "redirect_uris": [
                    "http://localhost:33418/callback"
                  ],
                  "grant_types": [
                    "authorization_code",
                    "refresh_token"
                  ],
                  "response_types": [
                    "code"
                  ],
                  "token_endpoint_auth_method": "none",
                  "scope": "instances:read docs:read logs:read"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_redirect_uri`, `invalid_client_metadata` ou `invalid_scope`.",
            "content": {
              "application/json": {
                "example": {
                  "error": "invalid_redirect_uri",
                  "error_description": "redirect_uri não aceito: http://evil.com/cb. Use https://, ou http:// apenas em localhost."
                }
              }
            }
          },
          "429": {
            "description": "Muitos registros desta origem.",
            "content": {
              "application/json": {
                "example": {
                  "error": "too_many_requests",
                  "error_description": "Muitos registros desta origem. Espere antes de tentar de novo."
                }
              }
            }
          }
        }
      }
    },
    "/oauth/authorize": {
      "servers": [
        {
          "url": "https://auth.fluxoapi.com.br",
          "description": "Authorization Server"
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Tela de consentimento",
        "operationId": "oauthAuthorize",
        "security": [],
        "description": "Devolve **HTML**, não JSON: quem está do outro lado é uma pessoa. Nada é concedido aqui — o pedido fica guardado por 5 minutos e vira código só depois do clique em \"Permitir\".\n\nSem sessão, redireciona para o login e volta para cá depois. PKCE com S256 é obrigatório; `plain` é recusado. `resource` (RFC 8707) é obrigatório e vira a audiência do token.",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Igualdade exata com uma das registradas. Prefixo permitiria roubar o código com `https://legitimo.com.evil.com`."
          },
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "code"
              ]
            }
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 43,
              "maxLength": 128
            }
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            }
          },
          {
            "name": "resource",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "O recurso para o qual o token vale. Precisa ser o servidor MCP."
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Devolvido intacto no redirect."
          }
        ],
        "responses": {
          "200": {
            "description": "A tela de consentimento, em HTML.",
            "content": {
              "text/html": {}
            }
          },
          "302": {
            "description": "Sem sessão: vai para o login. Ou erro do OAuth devolvido pelo `redirect_uri`, com `error`, `error_description` e `iss`."
          },
          "400": {
            "description": "`invalid_client` — cliente desconhecido, revogado, ou de outra conta.",
            "content": {
              "application/json": {
                "example": {
                  "error": "invalid_client",
                  "error_description": "Cliente desconhecido ou revogado."
                }
              }
            }
          }
        }
      }
    },
    "/oauth/token": {
      "servers": [
        {
          "url": "https://auth.fluxoapi.com.br",
          "description": "Authorization Server"
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Troca o código por token, ou renova",
        "operationId": "oauthToken",
        "security": [],
        "description": "Aceita `application/x-www-form-urlencoded`. Dois fluxos:\n\n- `authorization_code` — troca o código, conferindo o `code_verifier` do PKCE em tempo constante. **Código reutilizado revoga todos os tokens daquele cliente**: se alguém interceptou e chegou primeiro, o legítimo derruba a sessão do atacante ao tentar.\n- `refresh_token` — rotaciona: o antigo morre no uso. Apresentar um refresh já rotacionado é sinal de vazamento e **revoga a linhagem inteira**.\n\nLimite de 30 requisições por minuto por origem.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "code": {
                    "type": "string"
                  },
                  "code_verifier": {
                    "type": "string"
                  },
                  "redirect_uri": {
                    "type": "string"
                  },
                  "refresh_token": {
                    "type": "string"
                  },
                  "resource": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token emitido.",
            "content": {
              "application/json": {
                "example": {
                  "access_token": "fxa_…",
                  "token_type": "Bearer",
                  "expires_in": 3600,
                  "refresh_token": "fxr_…",
                  "scope": "instances:read docs:read logs:read"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_grant`, `invalid_request`, `invalid_target` ou `unsupported_grant_type`.",
            "content": {
              "application/json": {
                "example": {
                  "error": "invalid_grant",
                  "error_description": "code_verifier não confere com o code_challenge."
                }
              }
            }
          },
          "429": {
            "description": "Muitas requisições.",
            "content": {
              "application/json": {
                "example": {
                  "error": "slow_down",
                  "error_description": "Muitas requisições."
                }
              }
            }
          }
        }
      }
    },
    "/oauth/revoke": {
      "servers": [
        {
          "url": "https://auth.fluxoapi.com.br",
          "description": "Authorization Server"
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Revoga um token",
        "operationId": "oauthRevoke",
        "security": [],
        "description": "Aceita access token ou refresh token. Responde **200 sempre**, inclusive para token desconhecido (RFC 7009) — distinguir permitiria sondar quais tokens existem.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sempre 200, sem corpo."
          }
        }
      }
    },
    "/": {
      "servers": [
        {
          "url": "https://mcp.fluxoapi.com.br",
          "description": "Servidor MCP"
        }
      ],
      "post": {
        "tags": [
          "MCP"
        ],
        "summary": "Endpoint JSON-RPC do servidor MCP",
        "operationId": "mcpRpc",
        "security": [
          {
            "mcpBearer": []
          }
        ],
        "description": "Fala JSON-RPC 2.0. Métodos: `initialize`, `tools/list`, `tools/call`. Cada ferramenta exige um escopo; sem ele, o erro diz qual falta.\n\nÉ **somente leitura**: serve para um agente de código ajudar a integrar a API — ler a especificação, achar o `instance_id`, montar a assinatura, explicar um erro. Não é caminho para um agente falar com o cliente final.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "get_openapi",
                  "arguments": {
                    "path": "/instances"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta JSON-RPC.",
            "content": {
              "application/json": {
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "result": {
                    "content": []
                  }
                }
              }
            }
          },
          "401": {
            "description": "Sem token ou audiência errada. O cabeçalho `WWW-Authenticate` aponta o metadata do recurso.",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                },
                "example": "Bearer resource_metadata=\"https://mcp.fluxoapi.com.br/.well-known/oauth-protected-resource\""
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "MCP"
        ],
        "summary": "Descoberta do Authorization Server",
        "operationId": "mcpDiscover",
        "security": [],
        "description": "Sem token, responde 401 com `WWW-Authenticate` — é assim que o cliente descobre onde se autenticar, sem configuração manual.",
        "responses": {
          "401": {
            "description": "Aponta o metadata."
          },
          "405": {
            "description": "Autenticado: use POST.",
            "content": {
              "application/json": {}
            }
          }
        }
      }
    },
    "/public/pricing": {
      "servers": [
        {
          "url": "https://fluxoapi.com.br",
          "description": "Painel"
        }
      ],
      "get": {
        "tags": [
          "Público"
        ],
        "summary": "Tabela de preços",
        "operationId": "pricing",
        "security": [],
        "description": "Sem autenticação — é o que a página de planos consome.",
        "responses": {
          "200": {
            "description": "Faixas e preço por número.",
            "content": {
              "application/json": {}
            }
          }
        }
      }
    },
    "/public/connect/{token}": {
      "servers": [
        {
          "url": "https://fluxoapi.com.br",
          "description": "Painel"
        }
      ],
      "get": {
        "tags": [
          "Público"
        ],
        "summary": "Dados da tela de conexão de um link",
        "operationId": "connectLink",
        "security": [],
        "description": "O link que a empresa envia ao dono do número. Entrega só o necessário para a tela — nome da empresa, nome do número e a configuração do Embedded Signup. Nada de conversa, nada de identificador de outro número. Vale uma vez e expira; limite de 30 tentativas por minuto por origem.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dados da tela.",
            "content": {
              "application/json": {}
            }
          },
          "404": {
            "description": "Link vencido, já usado ou revogado.",
            "content": {
              "application/json": {}
            }
          },
          "429": {
            "description": "Muitas tentativas.",
            "content": {
              "application/json": {}
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Guias e painel",
    "url": "https://fluxoapi.com.br/docs/"
  },
  "tags": [
    {
      "name": "Instâncias",
      "description": "Os números conectados: estado, qualidade e vencimento do acesso."
    },
    {
      "name": "Mensagens",
      "description": "Enviar, listar e confirmar leitura."
    },
    {
      "name": "Conversas",
      "description": "O fio por contato, com a janela de 24 h."
    },
    {
      "name": "Modelos",
      "description": "Catálogo aprovado na Meta — o único caminho fora da janela de 24 h."
    },
    {
      "name": "Funil",
      "description": "Oportunidades e etapas."
    },
    {
      "name": "Serviço",
      "description": "Saúde e métricas. Não exigem assinatura HMAC."
    },
    {
      "name": "OAuth",
      "description": "Authorization Server do MCP — OAuth 2.1 com PKCE."
    },
    {
      "name": "MCP",
      "description": "Servidor MCP para agentes de código. Somente leitura."
    },
    {
      "name": "Público",
      "description": "Sem autenticação: preços e link de conexão."
    }
  ],
  "webhooks": {
    "message.received": {
      "post": {
        "summary": "Mensagem recebida de um contato",
        "operationId": "webhook_message_received",
        "description": "Dispara a cada mensagem que chega. **Reabre a janela de 24 h** — é a partir do horário deste evento que você tem um dia para responder livremente.\n\nConfira SEMPRE a assinatura antes de agir. Cabeçalhos de cada entrega:\n\n- `X-Fluxo-Event-Id` — sua chave de idempotência. A entrega é at-least-once: o mesmo evento pode chegar duas vezes, e processar duas vezes é problema seu, não defeito nosso.\n- `X-Fluxo-Event-Type` — o tipo, repetido no corpo.\n- `X-Fluxo-Timestamp` — Unix em segundos.\n- `X-Fluxo-Signature` — `v1=<hex>` do HMAC-SHA256, com o segredo `whsec_…` da instância, sobre `timestamp`, barra-n, e o **corpo bruto**. Durante uma rotação, o cabeçalho traz duas assinaturas separadas por vírgula: aceite qualquer uma.\n\nResponda 2xx rápido. Fora disso reenviamos em 0 s, 30 s, 2 min, 10 min, 1 h, 6 h e 24 h; depois a entrega vai para a fila de falhas. Endpoint 24 h sem nenhuma entrega bem-sucedida é **desligado automaticamente**, e o painel avisa.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Igual ao `X-Fluxo-Event-Id`."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "message.received"
                    ]
                  },
                  "instance_id": {
                    "type": "string"
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "conversation_id": { "type": "string" },
                      "message_id": { "type": "string" },
                      "from": { "type": "string" },
                      "type": { "type": "string", "description": "`text`, `image`, `audio`, `video`, `document`, `sticker`, `location`, `contacts`, `reaction`; `interactive` quando o contato tocou num botão ou linha; `button` para resposta rápida de modelo. Nos tipos de mídia, `media` vem preenchido." },
                      "text": { "type": "string", "description": "Em clique, o título escolhido. Em mídia, a legenda (documento sem legenda traz o nome do arquivo); pode vir vazio." },
                      "sent_from_phone": { "type": "boolean" },
                      "media": { "$ref": "#/components/schemas/Media" },
                      "reply": { "$ref": "#/components/schemas/InteractiveReply" },
                      "reply_to_wamid": { "type": "string", "description": "wamid da mensagem que foi respondida, quando a Meta informa." }
                    }
                  }
                }
              },
              "examples": {
                "documento": {
                  "summary": "Documento (com mídia para baixar)",
                  "value": {
                    "id": "evt_01M1HQ9K2V7D4FRX8NCP3TQZWA",
                    "type": "message.received",
                    "instance_id": "inst_01M1HQ7SE7C7QFBGMN8Y8ZNKAP",
                    "created_at": "2026-09-04T11:31:02.000Z",
                    "data": {
                      "conversation_id": "conv_01M1…",
                      "message_id": "msg_01JB8YF2K9QX4TMA",
                      "from": "5519991802264",
                      "type": "document",
                      "text": "comprovante.pdf",
                      "sent_from_phone": false,
                      "media": {
                        "id": "1037543291543636",
                        "mime_type": "application/pdf",
                        "filename": "comprovante.pdf",
                        "caption": null,
                        "sha256": "Ofz1b8vT2k…",
                        "download_path": "/v1/messages/msg_01JB8YF2K9QX4TMA/media"
                      }
                    }
                  }
                },
                "texto": {
                  "summary": "Texto",
                  "value": {
                    "id": "evt_01M1HQ7SE7C7QFBGMN8Y8ZNKAP",
                    "type": "message.received",
                    "instance_id": "inst_01M1HQ7SE7C7QFBGMN8Y8ZNKAP",
                    "created_at": "2026-09-04T11:27:36.000Z",
                    "data": {
                      "conversation_id": "conv_01M1…",
                      "message_id": "msg_01M1…",
                      "from": "5519996155951",
                      "type": "text",
                      "text": "oi",
                      "sent_from_phone": false
                    }
                  }
                },
                "clique": {
                  "summary": "Clique em linha de lista",
                  "value": {
                    "id": "evt_01M1HQ9K2P4R6T8V0X2Z4B6D8F",
                    "type": "message.received",
                    "instance_id": "inst_01M1HQ7SE7C7QFBGMN8Y8ZNKAP",
                    "created_at": "2026-09-04T11:29:02.000Z",
                    "data": {
                      "conversation_id": "conv_01M1…",
                      "message_id": "msg_01M1…",
                      "from": "5519996155951",
                      "type": "interactive",
                      "text": "09:00",
                      "sent_from_phone": false,
                      "reply": { "kind": "list_reply", "id": "h09", "title": "09:00", "description": "Com o corretor Paulo" },
                      "reply_to_wamid": "wamid.HBgLNTUxOTk5NjE1NTk1MRUCABEYEjA…"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recebido. Qualquer 2xx encerra a entrega."
          }
        }
      }
    },
    "message.sent_from_phone": {
      "post": {
        "summary": "O dono respondeu pelo app do celular (coexistência)",
        "operationId": "webhook_message_sent_from_phone",
        "description": "Só existe em instâncias com coexistência. Dispara quando alguém da empresa envia uma mensagem pelo **app WhatsApp Business do celular** — a Meta avisa por eco, e a mensagem é gravada como de **saída** (`direction: outbound`, `sent_from_phone: true`). Aparece também em `GET /messages`.\n\n**Não é `message.received`, de propósito.** Quem integra usa `message.received` para decidir o que responder; tratar a resposta do próprio dono como mensagem do cliente faria o robô responder ao humano da casa — e, em atendimento automático, responder a si mesmo em laço. Use este evento para saber que um humano assumiu a conversa (por exemplo, pausar o robô).\n\nDiferenças em relação a uma mensagem recebida:\n\n- `to` é o número do **cliente**; não há `from`.\n- **Não reabre a janela de 24 h** — só o cliente reabre.\n- A entrega e a leitura chegam depois em `message.status`, casadas pelo `message_id`.\n\nConfira SEMPRE a assinatura antes de agir. Cabeçalhos de cada entrega:\n\n- `X-Fluxo-Event-Id` — sua chave de idempotência. A entrega é at-least-once: o mesmo evento pode chegar duas vezes, e processar duas vezes é problema seu, não defeito nosso.\n- `X-Fluxo-Event-Type` — o tipo, repetido no corpo.\n- `X-Fluxo-Timestamp` — Unix em segundos.\n- `X-Fluxo-Signature` — `v1=<hex>` do HMAC-SHA256, com o segredo `whsec_…` da instância, sobre `timestamp`, barra-n, e o **corpo bruto**. Durante uma rotação, o cabeçalho traz duas assinaturas separadas por vírgula: aceite qualquer uma.\n\nResponda 2xx rápido. Fora disso reenviamos em 0 s, 30 s, 2 min, 10 min, 1 h, 6 h e 24 h; depois a entrega vai para a fila de falhas. Endpoint 24 h sem nenhuma entrega bem-sucedida é **desligado automaticamente**, e o painel avisa.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Igual ao `X-Fluxo-Event-Id`."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "message.sent_from_phone"
                    ]
                  },
                  "instance_id": {
                    "type": "string"
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "conversation_id": { "type": "string" },
                      "message_id": { "type": "string" },
                      "to": { "type": "string", "description": "Número do cliente que recebeu a resposta." },
                      "type": { "type": "string", "description": "Tipo da mensagem da Meta: `text`, `image`, …" },
                      "text": { "type": "string", "nullable": true },
                      "sent_from_phone": { "type": "boolean", "enum": [true] },
                      "media": { "$ref": "#/components/schemas/Media" }
                    }
                  }
                }
              },
              "example": {
                "id": "evt_01M1…",
                "type": "message.sent_from_phone",
                "instance_id": "inst_01M1…",
                "created_at": "2026-09-04T11:29:10.000Z",
                "data": {
                  "conversation_id": "conv_01M1…",
                  "message_id": "msg_01M1…",
                  "to": "5519996155951",
                  "type": "text",
                  "text": "Oi! Já vou te atender por aqui.",
                  "sent_from_phone": true
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recebido. Qualquer 2xx encerra a entrega."
          }
        }
      }
    },
    "message.status": {
      "post": {
        "summary": "Mudança de estado de uma mensagem enviada",
        "operationId": "webhook_message_status",
        "description": "O `rank` só cresce: 1 enviada, 2 entregue, 3 lida, 9 falhou. Um `delivered` atrasado não desfaz um `read` que já chegou — compare por `rank`, nunca por ordem de chegada. Em falha, vem também `error` com `code` e `message`.\n\nConfira SEMPRE a assinatura antes de agir. Cabeçalhos de cada entrega:\n\n- `X-Fluxo-Event-Id` — sua chave de idempotência. A entrega é at-least-once: o mesmo evento pode chegar duas vezes, e processar duas vezes é problema seu, não defeito nosso.\n- `X-Fluxo-Event-Type` — o tipo, repetido no corpo.\n- `X-Fluxo-Timestamp` — Unix em segundos.\n- `X-Fluxo-Signature` — `v1=<hex>` do HMAC-SHA256, com o segredo `whsec_…` da instância, sobre `timestamp`, barra-n, e o **corpo bruto**. Durante uma rotação, o cabeçalho traz duas assinaturas separadas por vírgula: aceite qualquer uma.\n\nResponda 2xx rápido. Fora disso reenviamos em 0 s, 30 s, 2 min, 10 min, 1 h, 6 h e 24 h; depois a entrega vai para a fila de falhas. Endpoint 24 h sem nenhuma entrega bem-sucedida é **desligado automaticamente**, e o painel avisa.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Igual ao `X-Fluxo-Event-Id`."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "message.status"
                    ]
                  },
                  "instance_id": {
                    "type": "string"
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "data": {
                    "type": "object"
                  }
                }
              },
              "example": {
                "id": "evt_01M1…",
                "type": "message.status",
                "instance_id": "inst_01M1…",
                "created_at": "2026-09-04T11:28:00.000Z",
                "data": {
                  "message_id": "msg_01M1…",
                  "conversation_id": "conv_01M1…",
                  "status": "delivered",
                  "rank": 2,
                  "at": "2026-09-04T11:27:59.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recebido. Qualquer 2xx encerra a entrega."
          }
        }
      }
    },
    "instance.status_changed": {
      "post": {
        "summary": "Estado, qualidade ou limite do número mudou",
        "operationId": "webhook_instance_status_changed",
        "description": "Qualidade caindo para `yellow` ou `red` é aviso antes da punição: a Meta reduz o limite diário e, no limite, desativa o número.\n\nConfira SEMPRE a assinatura antes de agir. Cabeçalhos de cada entrega:\n\n- `X-Fluxo-Event-Id` — sua chave de idempotência. A entrega é at-least-once: o mesmo evento pode chegar duas vezes, e processar duas vezes é problema seu, não defeito nosso.\n- `X-Fluxo-Event-Type` — o tipo, repetido no corpo.\n- `X-Fluxo-Timestamp` — Unix em segundos.\n- `X-Fluxo-Signature` — `v1=<hex>` do HMAC-SHA256, com o segredo `whsec_…` da instância, sobre `timestamp`, barra-n, e o **corpo bruto**. Durante uma rotação, o cabeçalho traz duas assinaturas separadas por vírgula: aceite qualquer uma.\n\nResponda 2xx rápido. Fora disso reenviamos em 0 s, 30 s, 2 min, 10 min, 1 h, 6 h e 24 h; depois a entrega vai para a fila de falhas. Endpoint 24 h sem nenhuma entrega bem-sucedida é **desligado automaticamente**, e o painel avisa.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Igual ao `X-Fluxo-Event-Id`."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "instance.status_changed"
                    ]
                  },
                  "instance_id": {
                    "type": "string"
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "data": {
                    "type": "object"
                  }
                }
              },
              "example": {
                "id": "evt_01M1…",
                "type": "instance.status_changed",
                "instance_id": "inst_01M1…",
                "created_at": "2026-09-04T12:00:00.000Z",
                "data": {
                  "field": "phone_number_quality_update",
                  "messaging_limit_24h": 1000,
                  "quality": "yellow"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recebido. Qualquer 2xx encerra a entrega."
          }
        }
      }
    },
    "instance.access_expiring": {
      "post": {
        "summary": "O acesso do número vence em até 7 dias",
        "operationId": "webhook_instance_access_expiring",
        "description": "Avisa com uma semana de antecedência. Vencido, o número para de enviar e receber até reconectar pelo painel.\n\nConfira SEMPRE a assinatura antes de agir. Cabeçalhos de cada entrega:\n\n- `X-Fluxo-Event-Id` — sua chave de idempotência. A entrega é at-least-once: o mesmo evento pode chegar duas vezes, e processar duas vezes é problema seu, não defeito nosso.\n- `X-Fluxo-Event-Type` — o tipo, repetido no corpo.\n- `X-Fluxo-Timestamp` — Unix em segundos.\n- `X-Fluxo-Signature` — `v1=<hex>` do HMAC-SHA256, com o segredo `whsec_…` da instância, sobre `timestamp`, barra-n, e o **corpo bruto**. Durante uma rotação, o cabeçalho traz duas assinaturas separadas por vírgula: aceite qualquer uma.\n\nResponda 2xx rápido. Fora disso reenviamos em 0 s, 30 s, 2 min, 10 min, 1 h, 6 h e 24 h; depois a entrega vai para a fila de falhas. Endpoint 24 h sem nenhuma entrega bem-sucedida é **desligado automaticamente**, e o painel avisa.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Igual ao `X-Fluxo-Event-Id`."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "instance.access_expiring"
                    ]
                  },
                  "instance_id": {
                    "type": "string"
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "data": {
                    "type": "object"
                  }
                }
              },
              "example": {
                "id": "evt_01M1…",
                "type": "instance.access_expiring",
                "instance_id": "inst_01M1…",
                "created_at": "2026-09-04T03:00:00.000Z",
                "data": {
                  "instance_id": "inst_01M1…",
                  "name": "Central",
                  "days_left": 5
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recebido. Qualquer 2xx encerra a entrega."
          }
        }
      }
    }
  }
}