{
  "overview": "O seu sistema chama a Partner API para cadastrar e consultar; a HookCloud chama o webhook HTTPS do seu sistema por POST para avisar mudanças. Use esses eventos para atualizar o cadastro e o painel do seu assinante. Consulte a Partner API na primeira carga ou para reconciliar uma informação perdida, antiga ou incompleta.",
  "rules": [
    "Cadastre e valide a URL em Endpoints. O endpoint deve estar habilitado e subscribed_events deve incluir o nome exato do evento ou *. A seleção de eventos HookCloud é diferente dos campos assinados no app da Meta. A URL de destino pode ser a mesma, mas cada origem tem seu contrato e sua assinatura.",
    "A requisição recebida no seu servidor é POST com Content-Type: application/json. Eventos originais da Meta usam object/entry/changes e X-Hub-Signature-256; eventos HookCloud usam os formatos abaixo e X-HookCloud-Event, X-HookCloud-Delivery, X-HookCloud-Timestamp e X-HookCloud-Signature. O desafio GET hub.verify_token serve somente para validar a URL.",
    "O segredo HMAC do endpoint (webhook_secret, prefixo hc_whsec_) é diferente da Partner API Key, do token de verificação GET e do token Meta. Ele é provisionado com o endpoint; o GET de consulta do endpoint não devolve esse segredo. Se você não guardou a credencial fornecida no provisionamento, trate a configuração com o administrador antes de liberar o receptor.",
    "Valide a assinatura HMAC SHA-256 sobre timestamp + ponto + corpo bruto, sem reserializar o JSON. Confira também o timestamp em segundos (por exemplo, tolerância de cinco minutos). Não escolha a origem apenas pelo nome de um campo; valide a assinatura apropriada antes de aceitar o conteúdo.",
    "Persista ou enfileire o evento de forma durável antes de responder HTTP 2xx. Timeout e respostas fora de 200–299 provocam novas tentativas; redirects não são seguidos. A entrega tem tentativas limitadas e pode terminar em dead_letter. Não há garantia de entrega exatamente uma vez ou de ordem.",
    "Para os eventos documentados, use payload.event_id quando presente; senão, payload.id. X-HookCloud-Delivery identifica a entrega para um endpoint e permanece o mesmo nas retentativas; não representa o cliente nem o número. Guarde o ID do evento para impedir processamento duplicado. Nos eventos antigos de ciclo de vida, id e event_id podem ser diferentes: prefira event_id.",
    "Cada evento tem seu próprio envelope. Alguns usam event e campos na raiz; outros usam type e data. Campos ausentes ou null não são prova de ausência de pagamento, empresa não verificada ou conexão desligada. Preserve campos desconhecidos e ignore tipos ainda não implementados após registrar a ocorrência.",
    "Também existem avisos de operações de cadastro, como hookcloud.partner.customer.updated, hookcloud.partner.customer.merged, hookcloud.partner.instance.moved, hookcloud.partner.customer.erasure_started, hookcloud.partner.customer.erasure_failed e hookcloud.partner.customer.erased. Esses contratos antigos enviam o payload da operação sem envelope universal: podem não ter id, event_id, event, type, partner_id ou created_at no body. Identifique o tipo pelo X-HookCloud-Event validado, deduplique pelo X-HookCloud-Delivery quando não houver ID de evento e reconcilie o cliente/instância pela Partner API. Não aplique o parser dos eventos de saúde a esses corpos.",
    "Compare connection_generation quando existir (é um UUID opaco, não um contador ordenável): um evento de uma autorização anterior não deve substituir a conexão atual. Use observed_at, checked_at ou evaluated_at do fato e não apenas a hora de chegada. Se houver geração diferente, empate ou dúvida de ordem, consulte get-partner-instance/get-partner-customer para reconciliar.",
    "GET get-partner-instance e list-partner-instances consultam o estado armazenado na HookCloud; não fazem uma nova leitura Graph a cada chamada. Use page e page_size nas listagens e percorra as páginas retornadas. Para uma conferência explícita use refresh-partner-instance-meta, com controle de frequência; não faça polling contínuo à Meta.",
    "A sincronização é eventual: a Meta pode levar tempo para propagar a mudança e a HookCloud usa fila e recuo após erros. Nem todo GET gera evento: saúde e perfil são notificados quando os dados relevantes mudam. O POST confirma uma observação, não uma garantia permanente de operação.",
    "Os exemplos abaixo são ilustrativos. IDs e datas são fictícios; connect_url contém somente um marcador. Não copie esses valores para produção nem registre credenciais, links de conexão ou conteúdo pessoal desnecessário em logs.",
    "O perfil opcional meta_profile possui cache de até 6 horas, separado da validade de 45 minutos da saúde. Uma nova observação relevante pode antecipar a leitura, respeitando fila e backoff. Mudanças materiais de limite/username seguem em hookcloud.instance.health_updated; apenas a passagem do tempo ou mudança de checked_at não cria um evento repetido. GETs locais e o refresh normal podem reutilizar esse cache: refreshed_at geral não substitui checked_at de cada campo.",
    "A rotina normal de reconciliação usa 30 minutos mais uma distribuição de 0 a 60 segundos, além do tempo de cron/fila. Evento não garante leitura imediata: somente o domínio pertinente pode ser reavaliado, respeitando piso de 15 minutos, orçamento de tentativas, cache e backoff. Dados empresariais têm cache de 24 horas; pagamento, 6 horas ou 24 horas quando permission_error. Atualização manual não contorna esses limites.",
    "instance.identity_replaced informa uma troca confirmada de IDs Meta, automática na reconexão do mesmo número ou explícita pelo contrato avançado. A instance_key permanece. Telefones enviados pelo navegador não autorizam substituição; a HookCloud exige prova da Meta e encerramento anterior.",
    "instance.meta_link_removed é emitido após confirmação assinada da perda de vínculo. O evento usa data.meta_link_state com evidence_source; o GET expõe a projeção meta_link com source. Não confunda esses nomes nem use erros Graph 100/33/190 como prova de remoção."
  ],
  "events": [
    {
      "id": "hookcloud.instance.health_updated",
      "title": "Saúde, pagamento Meta e verificação da empresa",
      "summary": "Atualiza o diagnóstico do assinante: conexão técnica, mensagens, pagamento e verificação empresarial. Também transporta observações opcionais de limite Meta e username comercial, sem usá-las para declarar saúde positiva ou negativa.",
      "when": "Quando a HookCloud registra mudança relevante de saúde, pagamento, verificação empresarial ou perfil opcional (limite e username). Atualizar somente checked_at ou a marca stale do perfil opcional não gera evento periódico. Uma consulta sem mudança material não garante outra notificação.",
      "identity": "Use partner_id + instance_id para localizar o número; customer_id é o UUID do cliente na HookCloud. Os dados ficam em connection_health na raiz, não em data. connection_generation identifica a autorização vigente.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111111",
        "event_id": "11111111-1111-4111-8111-111111111111",
        "created_at": "2026-10-02T15:00:02Z",
        "schema_version": "partner-health-v2",
        "event": "hookcloud.instance.health_updated",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "customer_id": "44444444-4444-4444-8444-444444444444",
        "connection_generation": "55555555-5555-4555-8555-555555555555",
        "observed_at": "2026-10-02T15:00:00Z",
        "connection_health": {
          "state": "ready",
          "ready_to_operate": true,
          "checked_at": "2026-10-02T15:00:00Z",
          "connection_status": "connected",
          "authorization_status": "available",
          "callback_status": "active",
          "inventory_state": "active",
          "meta_account_state": "no_explicit_restriction_observed",
          "messaging_capability": {
            "state": "available",
            "reason": null
          },
          "meta_payment": {
            "status": "no_issue_detected",
            "method_presence": "unknown",
            "issue_codes": [],
            "source": "graph_health",
            "method_source": null,
            "checked_at": "2026-10-02T15:00:00Z",
            "observed_at": "2026-10-02T15:00:00Z",
            "stale": false,
            "unavailable_reason": "permission_error"
          },
          "business_verification": {
            "status": "verified",
            "source": "graph_business",
            "checked_at": "2026-10-02T15:00:00Z",
            "observed_at": "2026-10-02T15:00:00Z",
            "stale": false,
            "unavailable_reason": null
          },
          "evidence": {
            "schema_version": "partner-health-v2",
            "complete": true,
            "stale": false,
            "truncated": false,
            "checked_at": "2026-10-02T15:00:00Z",
            "source": "graph",
            "identity_match": true
          },
          "allowed_actions": [
            "view_history",
            "refresh_health",
            "disconnect"
          ],
          "operation": null,
          "observation_scope": "current",
          "verification_status": "verified",
          "issues": [],
          "meta_profile": {
            "messaging_limit": {
              "status": "available",
              "source": "graph_phone_portfolio_limit",
              "checked_at": "2026-10-02T15:00:00Z",
              "unavailable_reason": null,
              "stale": false,
              "tier": "TIER_2K",
              "scope": "business_portfolio"
            },
            "username": {
              "status": "available",
              "source": "graph_phone_username",
              "checked_at": "2026-10-02T15:00:00Z",
              "unavailable_reason": null,
              "stale": false,
              "value": "empresa.exemplo",
              "lifecycle_status": "reserved"
            }
          }
        }
      },
      "fields": [
        {
          "name": "id / event_id",
          "type": "UUID",
          "description": "Neste evento os dois identificam o mesmo fato. Guarde event_id para deduplicar; não o use como instance_id."
        },
        {
          "name": "event / schema_version",
          "type": "string",
          "description": "event seleciona o tratamento hookcloud.instance.health_updated. partner-health-v2 identifica a versão do contrato; não é a versão da Graph API."
        },
        {
          "name": "partner_id / customer_id / instance_id",
          "type": "UUID",
          "description": "Identificam parceiro, cliente e número dentro da HookCloud. customer_id não é o external_customer_id do seu sistema. Mantenha esse vínculo ao cadastrar o cliente."
        },
        {
          "name": "connection_generation",
          "type": "UUID",
          "description": "Identifica a geração da conexão. Se diferir da geração salva, confirme o estado atual pelo GET antes de aplicar a mudança; não reative dados de uma conexão anterior."
        },
        {
          "name": "created_at / observed_at",
          "type": "data ISO 8601",
          "description": "created_at é a criação do evento; observed_at é a observação que motivou a atualização. A hora de recebimento no seu servidor pode ser posterior por fila ou retentativa."
        },
        {
          "name": "connection_health.state / ready_to_operate",
          "type": "enum / boolean",
          "description": "state: ready (disponível segundo a observação), limited (atenção/limitação), blocked (operação impedida) ou unknown (não confirmado). ready_to_operate é a conclusão operacional da HookCloud; não a recalcule apenas pelo pagamento ou por status=connected."
        },
        {
          "name": "connection_health.connection_status",
          "type": "enum",
          "description": "Estado técnico: created, pending_signup, connected, paused, inactive, canceled, error ou unknown. connected significa autorização concluída, não garantia de envio."
        },
        {
          "name": "connection_health.authorization_status / callback_status",
          "type": "enum / enum",
          "description": "authorization_status: available, unavailable ou unknown. callback_status descreve o destino dos eventos: active, configured, pending, error, failed, removed, removing, inactive ou unknown. São dimensões separadas da qualidade e do pagamento."
        },
        {
          "name": "connection_health.inventory_state / meta_account_state",
          "type": "enum / enum",
          "description": "inventory_state: active, attention, restricted, inactive ou unknown. meta_account_state: disabled, restricted, no_explicit_restriction_observed ou unknown. Pendência de pagamento isolada não deve virar rótulo de banimento."
        },
        {
          "name": "connection_health.messaging_capability.state / reason",
          "type": "enum / string ou null",
          "description": "state: available, limited, blocked ou unknown. reason explica o diagnóstico, por exemplo payment_setup, meta_restriction, connection_not_operational ou evidence_unavailable. Exiba a razão concreta e as issues; não prometa entrega de uma mensagem com base só neste campo."
        },
        {
          "name": "connection_health.meta_payment.status",
          "type": "enum",
          "description": "confirmed: fonte de cobrança identificada na WABA exata; não confirma cartão específico nem sucesso futuro. no_issue_detected: leitura de saúde completa sem a pendência 141006, sem confirmar método. action_required: pendência explícita que requer conferência. unknown: falta de evidência atual. Não transforme no_issue_detected em cartão confirmado."
        },
        {
          "name": "connection_health.meta_payment.method_presence",
          "type": "enum",
          "description": "identified: a leitura financeira identificou uma fonte de cobrança. not_confirmed: essa leitura não confirmou uma fonte. unknown: não foi possível determinar. Não descreve bandeira, validade, saldo ou dados de cartão."
        },
        {
          "name": "connection_health.meta_payment.issue_codes / source / method_source",
          "type": "array de números / string ou null",
          "description": "issue_codes preserva 141006 quando observado. source pode ser graph_health, graph_funding ou null; method_source é graph_funding ou null. Ausência de permissão financeira não comprova ausência de cartão."
        },
        {
          "name": "connection_health.meta_payment.checked_at / observed_at / stale / unavailable_reason",
          "type": "datas ou null / boolean / string ou null",
          "description": "Datas da evidência e sinal de desatualização. unavailable_reason explica a indisponibilidade, como permission_error ou identity_mismatch. Exemplo acima: a saúde não tem pendência, mas a permissão financeira não permite confirmar o método."
        },
        {
          "name": "connection_health.business_verification.status / source",
          "type": "enum / string ou null",
          "description": "verified, not_verified, pending ou unknown; source é graph_business, graph_waba ou null. Trata da empresa/portfólio, não da aprovação do nome do número e não de selo comercial."
        },
        {
          "name": "connection_health.business_verification.checked_at / observed_at / stale / unavailable_reason",
          "type": "datas ou null / boolean / string ou null",
          "description": "Informam quando o estado empresarial foi consultado/observado e se a evidência ainda é atual; não são a data exata em que a Meta aprovou a empresa. unknown não equivale a empresa reprovada."
        },
        {
          "name": "connection_health.evidence",
          "type": "objeto",
          "description": "schema_version indica o contrato; complete informa a completude; stale marca evidência antiga; truncated marca leitura parcial; checked_at e source (graph/stored) informam observação e origem; identity_match confirma correspondência à identidade atual."
        },
        {
          "name": "connection_health.issues[]",
          "type": "array de objetos",
          "description": "Cada item contém code, severity (info/warning/error/critical), message e action. domain e scope podem acompanhar a classificação, mas não são garantidos em avisos sintetizados. message explica o problema; action orienta a correção. Não converta qualquer warning em banimento."
        },
        {
          "name": "connection_health.allowed_actions",
          "type": "array de strings",
          "description": "Ações autorizadas pelo diagnóstico: view_history, refresh_health, disconnect, connect ou contact_support conforme o caso. Respeite também a autorização e as validações da API no momento da execução."
        },
        {
          "name": "connection_health.operation",
          "type": "objeto ou null",
          "description": "Quando presente: id, kind, status, generation, started_at, finished_at, lease_until, result_code, steps, remote_cleanup_confirmed e capacity_retained. processing não é conclusão; failed/needs_review/stale pedem conferência. Limpeza remota e liberação de capacidade são estados separados."
        },
        {
          "name": "connection_health.observation_scope / verification_status",
          "type": "enum / enum",
          "description": "observation_scope é current ou historical: não use uma fotografia histórica para declarar conexão atual ativa. verification_status é verified ou pending e trata da conferência do diagnóstico, não da verificação empresarial."
        },
        {
          "name": "connection_health.meta_profile",
          "type": "objeto",
          "description": "Observações opcionais de messaging_limit e username. São independentes da saúde operacional. Sua ausência em um payload antigo ou indisponibilidade não significa limite zero, ausência de username, bloqueio ou disponibilidade confirmada."
        },
        {
          "name": "connection_health.meta_profile.messaging_limit.status / tier",
          "type": "enum / string ou null",
          "description": "status=available informa que há uma faixa válida e atual observada; unavailable indica que não foi possível confirmar. tier pode ser TIER_250, TIER_2K, TIER_10K, TIER_100K ou TIER_UNLIMITED, entre valores que a Meta venha a retornar. Não é saldo restante, consumo, quantidade de mensagens do dia ou franquia paga na HookCloud. Null não significa zero nem ilimitado."
        },
        {
          "name": "connection_health.meta_profile.messaging_limit.scope / source",
          "type": "string ou null",
          "description": "business_portfolio e graph_phone_portfolio_limit vêm do campo moderno whatsapp_business_manager_messaging_limit: limite compartilhado pelos números do portfólio, para usuários únicos fora da janela de atendimento em 24 horas móveis. phone_legacy e graph_phone_legacy_limit identificam fallback antigo messaging_limit_tier; não some como capacidade extra nem trate esse fallback como nova confirmação do portfólio. TIER_UNLIMITED não elimina rate limits e outras políticas."
        },
        {
          "name": "connection_health.meta_profile.username.status / value / lifecycle_status",
          "type": "enum / string ou null",
          "description": "status=available confirma que o texto do username foi observado, não que esteja ativo. value é o username comercial; lifecycle_status é approved, reserved ou null quando não confirmado. reserved significa reservado, não aprovado/ativo. available com lifecycle_status=null exige rótulo neutro de estado não confirmado. Username comercial não é BSUID nem destinatário para o envio de mensagens."
        },
        {
          "name": "connection_health.meta_profile.messaging_limit e username: checked_at / source / stale / unavailable_reason",
          "type": "data ISO ou null / string ou null / boolean",
          "description": "Cada observação tem seu próprio horário, origem, validade e motivo de indisponibilidade. O cache opcional tem até 6 horas e respeita a identidade e geração da conexão. Expiração ou identidade diferente neutraliza tier/value/status de ciclo de vida; não reaproveite metadata antigo. permission_error, field_unavailable, rate_limited, timeout, identity_mismatch e stale explicam limitações sem afirmar ausência do dado. source de username é graph_phone_username ou null."
        }
      ],
      "actions": [
        "Atualize apenas a instância identificada, conservando a data e a geração da observação.",
        "Mostre pagamento Meta, empresa verificada, conexão e capacidade de envio em indicadores separados.",
        "No exemplo, exiba Empresa verificada e Sem pendência detectada; não exiba Cartão confirmado.",
        "Se a observação for antiga, parcial ou de geração desconhecida, reconcilie pelo GET antes de substituir o estado atual.",
        "Use o limite Meta como faixa informativa do portfólio, sem multiplicá-lo pelo número de instâncias e sem apresentar um saldo de mensagens que este payload não calcula.",
        "Exiba username reservado e situação não confirmada separadamente de username aprovado. Atualize seu cache pelos eventos materiais; não dispare GETs diretos à Meta para cada card."
      ],
      "reconcile": [
        "get-partner-instance",
        "list-partner-instances",
        "get-partner-instance-history",
        "refresh-partner-instance-meta"
      ]
    },
    {
      "id": "hookcloud.instance.updated",
      "title": "Qualidade, nome aprovado e portfólio do número",
      "summary": "Mantém o card do número atualizado com o nome e a qualidade informados pela Meta e os nomes do portfólio empresarial e da conta WhatsApp.",
      "when": "Quando mudam instance_name, verified_name, name_status, quality_rating, meta_business_name ou meta_waba_name. A mudança de um desses campos gera uma fotografia do perfil abaixo.",
      "identity": "Campos na raiz, com event (não type/data). Use partner_id + instance_id; customer_id localiza o cliente interno. Compare connection_generation antes de atualizar uma reconexão.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111112",
        "event_id": "11111111-1111-4111-8111-111111111112",
        "schema_version": "partner-instance-v2",
        "event": "hookcloud.instance.updated",
        "created_at": "2026-10-02T15:01:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "customer_id": "44444444-4444-4444-8444-444444444444",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "instance_key": "hc_inst_EXEMPLO",
        "connection_generation": "55555555-5555-4555-8555-555555555555",
        "instance_name": "Empresa Exemplo Atendimento",
        "verified_name": "Empresa Exemplo Atendimento",
        "name_status": "APPROVED",
        "quality_rating": "GREEN",
        "meta_business_id": "BUSINESS_ID",
        "meta_business_name": "Portfólio Empresa Exemplo",
        "meta_waba_id": "WABA_ID",
        "meta_waba_name": "WhatsApp Empresa Exemplo",
        "meta_phone_number_id": "PHONE_NUMBER_ID"
      },
      "fields": [
        {
          "name": "id / event_id / event / schema_version",
          "type": "UUID / string",
          "description": "event_id identifica o fato para deduplicação. event é hookcloud.instance.updated e schema_version é partner-instance-v2."
        },
        {
          "name": "partner_id / customer_id / instance_id / instance_key",
          "type": "UUID / string",
          "description": "Vínculos internos e chave imutável da instância. Não confunda instance_key com um segredo nem com o telefone; guarde o relacionamento com seu external_customer_id."
        },
        {
          "name": "created_at / connection_generation",
          "type": "data ISO / UUID",
          "description": "Criação do evento e geração da autorização. Este payload não garante observed_at próprio; confirme pelo GET se não conseguir determinar se a atualização é mais recente."
        },
        {
          "name": "instance_name / verified_name / name_status",
          "type": "string ou null",
          "description": "instance_name é o nome apresentado no Partner; ele acompanha o verified_name quando a aprovação é confirmada. name_status traz o estado Meta, por exemplo APPROVED, AVAILABLE_WITHOUT_REVIEW ou PENDING_REVIEW. Nome pendente não prova bloqueio de mensagens nem empresa não verificada."
        },
        {
          "name": "quality_rating",
          "type": "string ou null",
          "description": "Qualidade informada pela Meta: normalmente GREEN, YELLOW, RED ou UNKNOWN. RED é um sinal de qualidade; não é sinônimo de banimento. Para o bloqueio de novas conexões use customer_risk.block_new_connections."
        },
        {
          "name": "meta_business_id / meta_business_name",
          "type": "string ou null",
          "description": "ID e nome do portfólio empresarial associado, quando disponíveis. A presença de um nome não comprova verificação da empresa; leia business_verification no diagnóstico."
        },
        {
          "name": "meta_waba_id / meta_waba_name / meta_phone_number_id",
          "type": "string ou null",
          "description": "Identificadores da conta WhatsApp e do número na Meta e nome da conta. phone_number_id é um ID técnico, não o telefone visível e não o BSUID do destinatário."
        }
      ],
      "actions": [
        "Atualize nome, qualidade e associação ao portfólio no card desse número.",
        "Guarde o nome retornado pela HookCloud; não mantenha um nome provisório diferente depois da aprovação.",
        "Não derive pagamento, verificação empresarial ou banimento apenas de qualidade/name_status. Use o evento de saúde e o de risco para esses indicadores."
      ],
      "reconcile": [
        "get-partner-instance",
        "list-partner-instances"
      ]
    },
    {
      "id": "hookcloud.customer.risk_updated",
      "title": "BAN, RED e restrição de novas conexões do cliente",
      "summary": "Atualiza a política individual de risco do cliente do parceiro, preservando os números já conectados.",
      "when": "Quando muda a política calculada com as evidências retidas: contagens, bloqueio, cobertura ou prazos da janela. A proteção é aplicada no servidor mesmo que seu sistema não processe o webhook.",
      "identity": "Este evento tem event e type iguais; customer_id fica na raiz e data.partner_customer_id repete o cliente. Não é uma restrição de todo o parceiro: o escopo é esse cliente.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111113",
        "event_id": "11111111-1111-4111-8111-111111111113",
        "event": "hookcloud.customer.risk_updated",
        "type": "hookcloud.customer.risk_updated",
        "created_at": "2026-10-02T15:02:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "customer_id": "44444444-4444-4444-8444-444444444444",
        "data": {
          "partner_customer_id": "44444444-4444-4444-8444-444444444444",
          "customer_risk": {
            "schema_version": "partner-customer-risk-v2",
            "scope": "customer",
            "partner_id": "22222222-2222-4222-8222-222222222222",
            "partner_customer_id": "44444444-4444-4444-8444-444444444444",
            "policy_basis": "grouped_incidents_rolling_window",
            "grouping": "exact_identity_between_explicit_restorations",
            "window_days": 30,
            "window_start": "2026-09-02T15:02:00Z",
            "evaluated_at": "2026-10-02T15:02:00Z",
            "ban_count_30d": 0,
            "red_count_30d": 5,
            "limits": {
              "ban": 20,
              "red": 5
            },
            "block_new_connections": true,
            "manual_blocked": false,
            "risk_level": "enforced",
            "reasons": [
              "red_window_limit"
            ],
            "next_exit_at": "2026-10-03T10:00:00Z",
            "next_exit_quantity": 1,
            "next_unlock_at": "2026-10-03T10:00:00Z",
            "timezone": "UTC",
            "existing_connections_preserved": true,
            "history_started_at": "2026-09-01T00:00:00Z",
            "history_complete": true,
            "coverage_reason": "Counts use retained verified evidence; events before retained history are not reconstructed."
          }
        }
      },
      "fields": [
        {
          "name": "id / event_id / event / type / created_at",
          "type": "UUID / string / data",
          "description": "event_id deduplica o fato. event e type contêm hookcloud.customer.risk_updated. created_at é a criação do evento; a política tem sua própria evaluated_at."
        },
        {
          "name": "partner_id / customer_id / data.partner_customer_id",
          "type": "UUID",
          "description": "Identificam o parceiro e o cliente interno afetado. A política segue a identidade do cliente, não todos os assinantes do parceiro."
        },
        {
          "name": "data.customer_risk.schema_version / scope / partner_id / partner_customer_id",
          "type": "string / UUID",
          "description": "Contrato partner-customer-risk-v2, escopo customer e identidade do titular da política. Guarde esses dados junto com as contagens."
        },
        {
          "name": "data.customer_risk.ban_count_30d / red_count_30d / limits",
          "type": "inteiros / objeto",
          "description": "Contagens de incidentes agrupados nos últimos 30 dias e limites {ban:20,red:5}. Não conte cada reentrega do mesmo webhook como um novo BAN/RED."
        },
        {
          "name": "data.customer_risk.block_new_connections / manual_blocked / risk_level / reasons",
          "type": "boolean / enum / array",
          "description": "block_new_connections é a decisão efetiva. manual_blocked indica análise manual. risk_level é enforced, warning ou none. reasons pode conter ban_window_limit, red_window_limit e manual_review. Mesmo com vagas pagas, a HookCloud recusa novas conexões desse cliente enquanto bloqueado."
        },
        {
          "name": "data.customer_risk.policy_basis / grouping / window_days / window_start / evaluated_at",
          "type": "strings / inteiro / datas",
          "description": "Descrevem o agrupamento por identidade entre recuperações explícitas, a janela móvel de 30 dias e quando ela foi calculada. Use evaluated_at para evitar regressão por evento atrasado."
        },
        {
          "name": "data.customer_risk.next_exit_at / next_exit_quantity / next_unlock_at / timezone",
          "type": "datas ou null / inteiro / string",
          "description": "Próxima saída de incidente da janela, quantidade nessa saída e estimativa de desbloqueio conforme as evidências atuais. UTC é o fuso dos valores; formate para o usuário. Novos incidentes podem alterar o prazo. Bloqueio manual deixa next_unlock_at null."
        },
        {
          "name": "data.customer_risk.existing_connections_preserved",
          "type": "boolean",
          "description": "true informa que a regra restringe novas conexões; ela não manda desconectar os números existentes."
        },
        {
          "name": "data.customer_risk.history_started_at / history_complete / coverage_reason",
          "type": "data / boolean / string",
          "description": "Informam a cobertura do histórico utilizado. history_complete=false exige mostrar a limitação de cobertura; não significa que o bloqueio deixou de valer."
        }
      ],
      "actions": [
        "Mostre as contagens, limites, razão e previsão ao lado do cliente e dos seus números.",
        "Desabilite a ação de adicionar número quando block_new_connections=true; mantenha a validação do servidor como decisão final.",
        "Não desconecte números existentes, não transfira a restrição a outros clientes e não recalcule incidentes somando webhooks."
      ],
      "reconcile": [
        "get-partner-customer",
        "get-partner-instance",
        "list-partner-instances"
      ]
    },
    {
      "id": "hookcloud.meta.template.updated",
      "title": "Aprovação, qualidade e conteúdo do template",
      "summary": "Atualiza o catálogo de templates da WABA para o seu sistema mostrar status, categoria, parâmetros e disponibilidade observada.",
      "when": "Quando a HookCloud detecta uma mudança no catálogo ou no estado do template por evento Meta ou sincronização. Deduplicação do produtor evita repetir a mesma transição como uma alteração nova.",
      "identity": "Envelope com type e data. Use partner_id + data.meta_waba_id + data.template_id; nome e idioma ajudam a localizar o modelo. Não há instance_id obrigatório: uma WABA pode estar associada a mais de um número.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111114",
        "type": "hookcloud.meta.template.updated",
        "created_at": "2026-10-02T15:03:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "data": {
          "meta_waba_id": "WABA_ID",
          "template_id": "TEMPLATE_ID",
          "name": "confirmacao_agendamento",
          "language": "pt_BR",
          "status": "APPROVED",
          "quality_status": "GREEN",
          "category": "UTILITY",
          "pending_category_change": null,
          "sendable": true,
          "missing_from_graph": false,
          "sync_status": "synced",
          "is_risky": false,
          "rejection_reason": null,
          "components": [
            {
              "type": "BODY",
              "text": "Seu agendamento foi confirmado."
            }
          ],
          "parameter_schema": {
            "total_parameters": 0,
            "parameter_format": "POSITIONAL",
            "parameters": [],
            "buttons": [],
            "components": [
              {
                "type": "BODY",
                "format": null,
                "text": "Seu agendamento foi confirmado.",
                "parameters": [],
                "example": null,
                "buttons": []
              }
            ]
          },
          "last_graph_sync_at": "2026-10-02T15:03:00Z",
          "source": "graph_sync"
        }
      },
      "fields": [
        {
          "name": "id / type / created_at / partner_id",
          "type": "UUID / string / data",
          "description": "id deduplica este evento; type é hookcloud.meta.template.updated. Este envelope não garante event_id, event, customer_id, instance_id nem previous_status. created_at é a criação do evento."
        },
        {
          "name": "data.meta_waba_id / template_id / name / language",
          "type": "string ou null",
          "description": "Conta WhatsApp, ID Meta, nome e idioma do template. Armazene por WABA e identidade do template, sem sobrescrever o catálogo de outra conta ou tradução."
        },
        {
          "name": "data.status / quality_status / category",
          "type": "string ou null",
          "description": "Status e qualidade conhecidos na Meta e categoria atual. status pode indicar aprovação, revisão, pausa ou rejeição; preserve valores novos. A categoria é distinta da qualidade."
        },
        {
          "name": "data.pending_category_change",
          "type": "objeto ou null",
          "description": "Quando presente, category é a categoria futura e effective_at é a data prevista. Não aplique como categoria atual antes de reconciliar sua vigência."
        },
        {
          "name": "data.sendable / missing_from_graph / sync_status / is_risky",
          "type": "booleans / string ou null",
          "description": "sendable é a elegibilidade observada, não garantia de entrega. missing_from_graph indica ausência na leitura de catálogo; sync_status informa a sincronização. is_risky sinaliza cuidado operacional. Não envie ignorando status, regras da Meta ou falha posterior."
        },
        {
          "name": "data.rejection_reason / components / parameter_schema",
          "type": "string ou null / array / objeto",
          "description": "Motivo conhecido de rejeição, componentes Meta e esquema dos parâmetros. Use-os para montar os campos do formulário do seu sistema; não envie o payload do webhook inteiro como body de mensagem."
        },
        {
          "name": "data.parameter_schema.total_parameters / parameter_format / parameters / buttons / components",
          "type": "inteiro / string / arrays",
          "description": "total_parameters conta os parâmetros identificados; parameter_format informa o formato, como POSITIONAL. parameters descreve component, index/name, kind e required quando aplicáveis; buttons e components organizam os campos por botão/componente. Um modelo sem variáveis tem total_parameters=0 e parameters=[]. Use o catálogo de detalhes para montar os componentes de envio da Meta."
        },
        {
          "name": "data.last_graph_sync_at / source",
          "type": "data ou null / string",
          "description": "Última sincronização Graph conhecida e origem desta atualização. A criação do evento e a leitura Graph podem ocorrer em horários diferentes."
        }
      ],
      "actions": [
        "Atualize o modelo e o idioma no catálogo da WABA correspondente.",
        "Recarregue os detalhes/parâmetros necessários pela Partner API se o cache local estiver incompleto.",
        "Ao enviar, use o contrato de mensagens da Meta. Um sendable=true não substitui o acompanhamento de sent/delivered/read/failed."
      ],
      "reconcile": [
        "list-partner-meta-templates",
        "get-partner-meta-template"
      ]
    },
    {
      "id": "hookcloud.meta.user_preferences.updated",
      "title": "Preferência de marketing do usuário",
      "summary": "Informa opt-out ou retomada de mensagens de marketing para o usuário do número exato.",
      "when": "Quando um evento válido de user_preferences da Meta é associado de forma inequívoca ao número e à WABA da instância.",
      "identity": "Envelope type/data. Use data.partner_instance_id + data.meta_phone_number_id e o user_id/wa_id de cada preferência; não espalhe a escolha por todos os números de uma WABA.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111115",
        "type": "hookcloud.meta.user_preferences.updated",
        "created_at": "2026-10-02T15:04:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "data": {
          "meta_waba_id": "WABA_ID",
          "partner_customer_id": "44444444-4444-4444-8444-444444444444",
          "partner_instance_id": "33333333-3333-4333-8333-333333333333",
          "meta_phone_number_id": "PHONE_NUMBER_ID",
          "user_preferences": [
            {
              "user_id": "BR.BSUID_EXEMPLO",
              "category": "marketing_messages",
              "value": "stop",
              "timestamp": "1790953440"
            }
          ]
        }
      },
      "fields": [
        {
          "name": "id / type / created_at / partner_id",
          "type": "UUID / string / data",
          "description": "id deduplica o evento. type seleciona a preferência. created_at é quando a HookCloud recebeu o evento Meta de origem; o horário da escolha individual é timestamp em cada preferência. Não há event_id/event garantidos."
        },
        {
          "name": "data.meta_waba_id / partner_customer_id / partner_instance_id / meta_phone_number_id",
          "type": "string / UUID",
          "description": "Delimitam a WABA, cliente e número da preferência; use esse escopo junto à identidade do usuário."
        },
        {
          "name": "data.user_preferences[].user_id / wa_id",
          "type": "string opcional",
          "description": "Pode haver BSUID (user_id), identidade legada wa_id ou ambos. Preserve o BSUID completo, inclusive prefixo e letras; não exija telefone para aplicar a preferência."
        },
        {
          "name": "data.user_preferences[].category / value / timestamp",
          "type": "string",
          "description": "category é marketing_messages; value é stop ou resume. timestamp é Unix em segundos como string e ordena a preferência do usuário. Não deixe um stop antigo sobrescrever um resume mais recente ou o inverso."
        }
      ],
      "actions": [
        "Em stop, interrompa marketing nesse escopo no seu sistema; em resume, registre a retomada respeitando as demais regras e permissões da Meta.",
        "Persista a preferência no seu backend. O GET de instância não é uma consulta do consentimento individual do usuário."
      ],
      "reconcile": []
    },
    {
      "id": "instance.created",
      "title": "Instância criada e link de conexão inicial",
      "summary": "Informa que o cadastro técnico foi criado. Pode acompanhar um link temporário para iniciar a autorização Meta.",
      "when": "Registrado junto com cada instância nova criada por create-customer-instance ou create-partner-instance. Reaproveitar a instância ou repetir a mesma operação idempotente não cria outro evento. A entrega é assíncrona e pode ser repetida; deduplique por event_id.",
      "identity": "Envelope event/data com instance_id na raiz. Guarde event_id para deduplicar, pois pode ser diferente de id.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111116",
        "event_id": "11111111-1111-4111-8111-111111111117",
        "event": "instance.created",
        "created_at": "2026-10-02T15:05:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "external_customer_id": "cliente-001",
        "data": {
          "status": "pending_signup",
          "instance_key": "hc_inst_EXEMPLO",
          "instance_name": "Nome provisório",
          "connect_url": "https://partner-connect.hookcloud.app/connect?token=TOKEN_TEMPORARIO_EXEMPLO"
        }
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at",
          "type": "UUID / string / data",
          "description": "event_id é o identificador persistido da ocorrência, preferido para deduplicar; id pode ser outro UUID do envelope. event é instance.created."
        },
        {
          "name": "partner_id / instance_id / external_customer_id",
          "type": "UUID / string",
          "description": "Relacionam o parceiro, a nova instância e o ID estável do cliente no seu sistema."
        },
        {
          "name": "data.status / instance_key / instance_name",
          "type": "string",
          "description": "pending_signup significa que a autorização ainda não terminou. A chave é gerada pela HookCloud; o nome pode ser provisório até a aprovação pela Meta."
        },
        {
          "name": "data.connect_url",
          "type": "URL sensível temporária ou null",
          "description": "URL sensível e temporária ou null. A sessão inicial tenta preencher este campo antes da entrega. O valor pode ser null quando create_connect_session=false, a geração da sessão falha ou o evento é entregue antes de o link ficar disponível. Use connect_url e connect_expires_at da resposta HTTP; se não houver link, consulte a instância e gere depois com create-connect-session. Receber este evento não confirma WhatsApp conectado. Não publique o link em logs."
        }
      ],
      "actions": [
        "Use instance_id, instance_key e o link retornados pela resposta HTTP da criação nas duas rotas. Não espere o webhook para continuar; o registro é durável, mas a entrega depende da disponibilidade do seu receptor.",
        "Guarde os identificadores e apresente o link ao cliente somente quando connect_url estiver presente; senão, gere a sessão quando necessário.",
        "Espere a conclusão e consulte a saúde antes de indicar que o número está operacional."
      ],
      "reconcile": [
        "get-partner-instance",
        "create-connect-session"
      ]
    },
    {
      "id": "connect_session.created",
      "title": "Novo link temporário de conexão",
      "summary": "Avisa que foi gerada uma nova sessão para conectar ou reconectar a instância.",
      "when": "Emitido pela chamada explícita create-connect-session. A sessão inicial criada internamente junto com uma instância não gera obrigatoriamente este evento.",
      "identity": "Envelope event/data com partner_id e instance_id; external_customer_id e connection_generation não são garantidos neste evento.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111118",
        "event_id": "11111111-1111-4111-8111-111111111119",
        "event": "connect_session.created",
        "created_at": "2026-10-02T15:06:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "data": {
          "connect_session_id": "66666666-6666-4666-8666-666666666666",
          "connect_url": "https://partner-connect.hookcloud.app/connect?token=TOKEN_TEMPORARIO_EXEMPLO",
          "expires_at": "2026-10-03T15:06:00Z"
        }
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at / partner_id / instance_id",
          "type": "UUID / string / data",
          "description": "event_id deduplica a ocorrência; instance_id identifica a conexão. event é connect_session.created, não uma confirmação de conexão."
        },
        {
          "name": "data.connect_session_id",
          "type": "UUID",
          "description": "ID da tentativa temporária, diferente de instance_id. Uma instância pode ter novas sessões ao longo do tempo."
        },
        {
          "name": "data.connect_url / expires_at",
          "type": "URL sensível / data ISO",
          "description": "Link autorizado e prazo dessa sessão. Use expires_at retornado como fonte do prazo; a configuração padrão atual é 24 horas. A expiração do link não desconecta um número já autorizado."
        }
      ],
      "actions": [
        "Use connect_session_id, connect_url e expires_at da resposta HTTP da chamada; não dependa da chegada do webhook para apresentar o link.",
        "Disponibilize o link somente ao cliente correto.",
        "Se expirado, crie nova sessão para a mesma instância, preservando a identidade e as regras de capacidade."
      ],
      "reconcile": [
        "get-partner-instance",
        "create-connect-session"
      ]
    },
    {
      "id": "instance.connected",
      "title": "Autorização da conexão concluída",
      "summary": "Informa a conclusão técnica; a liberação para operar depende do diagnóstico de saúde enviado ou consultado.",
      "when": "Ao concluir o fluxo autorizado da conexão. Alterar apenas o status administrativo para connected é rejeitado pela API: é necessária a prova da conexão.",
      "identity": "Envelope event/data; instance_id fica na raiz. Este contrato de ciclo de vida não garante connection_generation na raiz. Consulte o GET se precisar confirmar a geração vigente.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111120",
        "event_id": "11111111-1111-4111-8111-111111111121",
        "event": "instance.connected",
        "created_at": "2026-10-02T15:07:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "external_customer_id": "cliente-001",
        "data": {
          "status": "connected",
          "connection_health": {
            "state": "ready",
            "ready_to_operate": true,
            "checked_at": "2026-10-02T15:00:00Z",
            "connection_status": "connected",
            "authorization_status": "available",
            "callback_status": "active",
            "inventory_state": "active",
            "meta_account_state": "no_explicit_restriction_observed",
            "messaging_capability": {
              "state": "available",
              "reason": null
            },
            "meta_payment": {
              "status": "no_issue_detected",
              "method_presence": "unknown",
              "issue_codes": [],
              "source": "graph_health",
              "method_source": null,
              "checked_at": "2026-10-02T15:00:00Z",
              "observed_at": "2026-10-02T15:00:00Z",
              "stale": false,
              "unavailable_reason": "permission_error"
            },
            "business_verification": {
              "status": "verified",
              "source": "graph_business",
              "checked_at": "2026-10-02T15:00:00Z",
              "observed_at": "2026-10-02T15:00:00Z",
              "stale": false,
              "unavailable_reason": null
            },
            "evidence": {
              "schema_version": "partner-health-v2",
              "complete": true,
              "stale": false,
              "truncated": false,
              "checked_at": "2026-10-02T15:00:00Z",
              "source": "graph",
              "identity_match": true
            },
            "allowed_actions": [
              "view_history",
              "refresh_health",
              "disconnect"
            ],
            "operation": null,
            "observation_scope": "current",
            "verification_status": "verified",
            "issues": [],
            "meta_profile": {
              "messaging_limit": {
                "status": "available",
                "source": "graph_phone_portfolio_limit",
                "checked_at": "2026-10-02T15:00:00Z",
                "unavailable_reason": null,
                "stale": false,
                "tier": "TIER_2K",
                "scope": "business_portfolio"
              },
              "username": {
                "status": "available",
                "source": "graph_phone_username",
                "checked_at": "2026-10-02T15:00:00Z",
                "unavailable_reason": null,
                "stale": false,
                "value": "empresa.exemplo",
                "lifecycle_status": "reserved"
              }
            }
          },
          "ready_to_operate": true,
          "meta_business_id": "BUSINESS_ID",
          "waba_id": "WABA_ID",
          "phone_number_id": "PHONE_NUMBER_ID",
          "display_phone_number": "+55 11 99999-0000",
          "verified_name": "Empresa Exemplo Atendimento",
          "quality_rating": "GREEN",
          "account_mode": "LIVE",
          "code_verification_status": "VERIFIED",
          "app_subscribed": true,
          "remote_callback_scope": "phone_number",
          "remote_callback_status": "active",
          "remote_callback_uri": "https://seusistema.com/webhooks/whatsapp",
          "last_error": null
        }
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at / partner_id / instance_id / external_customer_id",
          "type": "UUID / string / data",
          "description": "event_id é a chave preferida de deduplicação; instance_id e external_customer_id relacionam a conexão ao cliente. O prefixo deste evento é instance., sem hookcloud.."
        },
        {
          "name": "data.status",
          "type": "string",
          "description": "connected informa a conclusão técnica do fluxo. Não implica ausência de pendências Meta nem garantia de envio."
        },
        {
          "name": "data.connection_health / ready_to_operate",
          "type": "objeto / boolean, quando presentes",
          "description": "No retorno do fluxo de conexão, o diagnóstico tem o significado explicado em hookcloud.instance.health_updated. Se ausentes, faça get-partner-instance. Conectado não significa pagamento confirmado ou mensagens liberadas."
        },
        {
          "name": "data.meta_business_id / waba_id / phone_number_id / display_phone_number",
          "type": "string ou null, quando presentes",
          "description": "Portfólio, conta WhatsApp, ID técnico do número e telefone visível. Observe que aqui os campos são waba_id/phone_number_id, sem prefixo meta_."
        },
        {
          "name": "data.verified_name / quality_rating / account_mode / code_verification_status",
          "type": "string ou null, quando presentes",
          "description": "Dados da observação Meta no fluxo concluído. Nome, qualidade, modo e verificação técnica são diferentes de verificação empresarial e pagamento."
        },
        {
          "name": "data.app_subscribed / remote_callback_scope / remote_callback_status / remote_callback_uri / last_error",
          "type": "boolean / strings ou null, quando presentes",
          "description": "Inscrição do app, escopo, estado e destino do callback e último erro. Use-os para diagnóstico; não confunda ausência de erro com saúde integral."
        }
      ],
      "actions": [
        "Atualize o estado técnico e consulte o diagnóstico completo quando não vier no evento.",
        "Só mostre disponibilidade operacional conforme connection_health; preserve pendências e orientações Meta."
      ],
      "reconcile": [
        "get-partner-instance",
        "get-partner-instance-history"
      ]
    },
    {
      "id": "instance.paused",
      "title": "Instância pausada",
      "summary": "Notifica a alteração do estado técnico para pausado.",
      "when": "Quando uma alteração de status gera o evento de pausa.",
      "identity": "Envelope event/data. Use partner_id e instance_id; os campos de saúde e geração não são obrigatórios.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111122",
        "event_id": "11111111-1111-4111-8111-111111111123",
        "event": "instance.paused",
        "created_at": "2026-10-02T15:08:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "external_customer_id": "cliente-001",
        "data": {
          "status": "paused",
          "reason": "Pausa solicitada"
        }
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at",
          "type": "UUID / string / data",
          "description": "Prefira event_id na deduplicação; event seleciona instance.paused."
        },
        {
          "name": "partner_id / instance_id / external_customer_id",
          "type": "UUID / string",
          "description": "Identidade da conexão e vínculo do cliente no seu sistema."
        },
        {
          "name": "data.status / reason",
          "type": "string / string ou null",
          "description": "Estado paused e motivo da alteração. Não comprova desregistro na Meta nem liberação de vaga."
        }
      ],
      "actions": [
        "Mostre a pausa e reconcilie saúde e capacidade pela Partner API."
      ],
      "reconcile": [
        "get-partner-instance"
      ]
    },
    {
      "id": "instance.inactive",
      "title": "Instância inativa",
      "summary": "Notifica a alteração do estado técnico para inativo.",
      "when": "Quando uma alteração de status gera o evento de inativação.",
      "identity": "Envelope event/data. Não existe um evento universal instance.disconnected neste catálogo: use o tipo efetivamente recebido e consulte a operação.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111124",
        "event_id": "11111111-1111-4111-8111-111111111125",
        "event": "instance.inactive",
        "created_at": "2026-10-02T15:09:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "external_customer_id": "cliente-001",
        "data": {
          "status": "inactive",
          "reason": "Inativação solicitada"
        }
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at",
          "type": "UUID / string / data",
          "description": "Prefira event_id para deduplicar; event seleciona instance.inactive."
        },
        {
          "name": "partner_id / instance_id / external_customer_id",
          "type": "UUID / string",
          "description": "Identificam a conexão e seu cliente."
        },
        {
          "name": "data.status / reason",
          "type": "string / string ou null",
          "description": "Estado inactive e motivo. A confirmação da limpeza remota e a capacidade ficam no diagnóstico/operation; não são inferidas apenas deste status."
        }
      ],
      "actions": [
        "Atualize o estado técnico e confira operation.remote_cleanup_confirmed e operation.capacity_retained no GET antes de concluir que a limpeza terminou e liberou vaga."
      ],
      "reconcile": [
        "get-partner-instance",
        "get-partner-instance-history"
      ]
    },
    {
      "id": "instance.canceled",
      "title": "Instância desconectada ou encerrada",
      "summary": "Notifica o encerramento técnico. Conclusões confirmadas de desconexão trazem a evidência adicional abaixo.",
      "when": "Depois da conclusão da desconexão pela HookCloud, iniciada pelo Portal, API ou Admin. O evento e o sininho são gravados na mesma transação. proof_source=phone_callback_removed_local_access_disabled + remote_cleanup_scope=hookcloud_integration confirma callback removido e credenciais locais desativadas, sem exigir revogação de uma autorização compartilhada. grant_revoked_readback + app_authorization permanece como prova distinta de revogação efetiva. Eventos legados sem prova não autorizam inventar limpeza remota.",
      "identity": "Envelope event/data identificado por partner_id e instance_id.",
      "example": {
        "id": "44444444-4444-4444-8444-444444444444",
        "event_id": "44444444-4444-4444-8444-444444444444",
        "event": "instance.canceled",
        "created_at": "2026-10-02T15:10:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "external_customer_id": "cliente-001",
        "data": {
          "status": "canceled",
          "reason": "Cancelamento solicitado",
          "deactivation_status": "completed",
          "operation_id": "44444444-4444-4444-8444-444444444444",
          "operation_kind": "disconnect",
          "connection_generation": "55555555-5555-4555-8555-555555555555",
          "actor_source": "partner_api_key",
          "occupies_slot": false,
          "slot_state": "released",
          "remote_callback_status": "removed",
          "remote_cleanup_confirmed": false,
          "remote_cleanup_required": true,
          "local_authorization": "disabled",
          "remote_authorization": "preserved_shared",
          "reconnect_required": true,
          "completed_at": "2026-10-02T15:10:00Z",
          "technical_disconnect_confirmed": true,
          "proof_source": "phone_callback_removed_local_access_disabled",
          "remote_cleanup_scope": "hookcloud_integration",
          "commercial_relationship": "unchanged",
          "phone_deregister_status": "not_attempted"
        },
        "operation_id": "44444444-4444-4444-8444-444444444444",
        "connection_generation": "55555555-5555-4555-8555-555555555555"
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at",
          "type": "UUID / string / data",
          "description": "Prefira event_id para deduplicar; event seleciona instance.canceled."
        },
        {
          "name": "partner_id / instance_id / external_customer_id",
          "type": "UUID / string",
          "description": "Identificam a conexão e seu cliente."
        },
        {
          "name": "data.status / reason",
          "type": "string / string ou null",
          "description": "Estado canceled e motivo. Não é o status da assinatura Stripe do parceiro e não comprova por si só a limpeza remota."
        },
        {
          "name": "operation_id / connection_generation",
          "type": "UUID",
          "description": "Identificam a conclusão e a geração da conexão. Ignore uma conclusão de geração antiga depois de reconectar. event_id permanece estável na repetição da entrega."
        },
        {
          "name": "data.remote_cleanup_confirmed / remote_cleanup_required",
          "type": "boolean",
          "description": "O contrato legado confirmed=true exige callback e deregistro. Em hookcloud_integration e app_authorization, confirmed=false é intencional: use technical_disconnect_confirmed, proof_source e remote_cleanup_scope para distinguir desativação local de revogação efetiva. Não invente deregistro nem retirada comercial."
        },
        {
          "name": "data.occupies_slot / slot_state / completed_at",
          "type": "boolean / string / data",
          "description": "false e released descrevem a vaga liberada na conclusão, com data. Releia o estado atual se a conexão já tiver outra geração."
        },
        {
          "name": "data.operation_kind / actor_source",
          "type": "string",
          "description": "disconnect ou billing_suspend; origem autenticada partner_api_key, partner_member_jwt, admin_jwt, system ou billing_worker. Origem não altera a validação."
        },
        {
          "name": "data.local_authorization / remote_authorization / reconnect_required",
          "type": "string / string / boolean",
          "description": "disabled impede uso da instância pela HookCloud. preserved_shared preserva a autorização Meta para outros números; unknown não confirma revogação. revoked exige grant_revoked_readback. Não invalide outras conexões ao receber este evento."
        },
        {
          "name": "data.technical_disconnect_confirmed / proof_source / remote_cleanup_scope",
          "type": "boolean / string",
          "description": "true + phone_callback_removed_local_access_disabled + hookcloud_integration confirma a retirada da rota e desativação local. true + grant_revoked_readback + app_authorization confirma revogação efetiva. Ambas exigem a geração exata; não provam exclusão da conta nem retirada comercial."
        },
        {
          "name": "data.commercial_relationship / phone_deregister_status",
          "type": "string",
          "description": "unchanged preserva a relação comercial. not_attempted informa que o número coexistente não foi deregistrado pela API. A empresa pode continuar listada como parceira no portfólio Meta."
        }
      ],
      "actions": [
        "Atualize a instância da mesma geração para Desconectado, guarde event_id e data da conclusão, e desative indicadores atuais de envio. Preserve informações Meta antigas como histórico.",
        "Se faltarem os campos de confirmação, consulte get-partner-instance: status canceled legado por si só não é prova remota.",
        "Não repita a desconexão ao receber o webhook. Retentativas de entrega são deduplicadas por event_id e X-HookCloud-Delivery.",
        "Na prova grant_revoked_readback, confira a mesma geração, status=canceled, occupies_slot=false e slot_state=released. Não espere remote_cleanup_confirmed=true, nem esconda uma parceria comercial somente porque a desconexão técnica terminou.",
        "Com proof_source=phone_callback_removed_local_access_disabled, mostre Desconectado da HookCloud. Se remote_authorization=preserved_shared, explique: Credenciais desativadas na HookCloud; autorização Meta compartilhada preservada para manter outros números."
      ],
      "reconcile": [
        "get-partner-instance",
        "get-partner-instance-history"
      ]
    },
    {
      "id": "instance.error",
      "title": "Erro no estado técnico da instância",
      "summary": "Avisa que uma alteração colocou a instância em estado de erro.",
      "when": "Quando o fluxo de status publica error.",
      "identity": "Envelope event/data; não trate erro genérico como BAN ou como ausência de cartão.",
      "example": {
        "id": "11111111-1111-4111-8111-111111111128",
        "event_id": "11111111-1111-4111-8111-111111111129",
        "event": "instance.error",
        "created_at": "2026-10-02T15:11:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "external_customer_id": "cliente-001",
        "data": {
          "status": "error",
          "reason": "Operação precisa de conferência"
        }
      },
      "fields": [
        {
          "name": "event_id / id / event / created_at",
          "type": "UUID / string / data",
          "description": "Prefira event_id para deduplicar; event seleciona instance.error."
        },
        {
          "name": "partner_id / instance_id / external_customer_id",
          "type": "UUID / string",
          "description": "Identificam a conexão e seu cliente."
        },
        {
          "name": "data.status / reason",
          "type": "string / string ou null",
          "description": "Estado error e explicação, quando disponível. Reconcilie issues, evidence e operation para orientar a ação correta."
        }
      ],
      "actions": [
        "Mostre atenção no número e consulte o diagnóstico; não repita automaticamente conexão, desconexão ou cobrança."
      ],
      "reconcile": [
        "get-partner-instance",
        "get-partner-instance-history"
      ]
    },
    {
      "id": "instance.identity_replaced",
      "title": "Identidade Meta substituída com confirmação",
      "summary": "Confirma o novo vínculo Meta da mesma instância lógica, preservando a instance_key e arquivando a identidade anterior.",
      "when": "Após confirmar novos IDs Meta na reconexão normal do mesmo número, com nova autorização, consulta à Graph e vínculo anterior encerrado, ou após uma sessão avançada mode=replace autorizada. O novo snapshot, o histórico, o evento e o aviso no sininho são gravados juntos. Gerar o link ou selecionar um telefone igual não confirma substituição.",
      "identity": "Envelope event/data com partner_id, customer_id, external_customer_id, instance_id e instance_key. connection_generation na raiz é a geração sucessora; data.replacement.connection_generation identifica a anterior.",
      "example": {
        "id": "44444444-4444-4444-8444-444444444444",
        "event_id": "44444444-4444-4444-8444-444444444444",
        "schema_version": "partner-instance-v2",
        "created_at": "2026-10-08T15:00:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "customer_id": "11111111-1111-4111-8111-111111111111",
        "external_customer_id": "cliente-001",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "instance_key": "hc_inst_exemplo",
        "event": "instance.identity_replaced",
        "operation_id": "77777777-7777-4777-8777-777777777777",
        "connection_generation": "66666666-6666-4666-8666-666666666666",
        "data": {
          "status": "connected",
          "replacement": {
            "id": "44444444-4444-4444-8444-444444444444",
            "status": "superseded",
            "subtype": "meta_identity_replacement",
            "connection_generation": "55555555-5555-4555-8555-555555555555",
            "successor_generation": "66666666-6666-4666-8666-666666666666",
            "phone_number_id": "100000000000001",
            "waba_id": "100000000000002",
            "business_id": "100000000000004",
            "display_phone_number": "+55 00 00000-0000",
            "verified_name": "Empresa de exemplo",
            "connected_at": "2026-10-01T15:00:00Z",
            "superseded_at": "2026-10-08T15:00:00Z",
            "reason": "explicit_meta_identity_replacement"
          },
          "phone_number_id": "100000000000003",
          "waba_id": "100000000000002",
          "instance_key_preserved": true
        }
      },
      "fields": [
        {
          "name": "id / event_id / event / schema_version",
          "type": "UUID / string",
          "description": "event_id deduplica; event=instance.identity_replaced e schema_version=partner-instance-v2 selecionam este contrato."
        },
        {
          "name": "partner_id / customer_id / external_customer_id / instance_id / instance_key",
          "type": "UUID / string / null",
          "description": "Identificam o parceiro, o cliente e a mesma instância lógica. Não crie uma segunda instância pelo simples recebimento do evento."
        },
        {
          "name": "operation_id / connection_generation",
          "type": "UUID",
          "description": "Operação de conexão concluída e sua geração sucessora. UUIDs são opacos; não ordene lexicalmente para decidir qual é atual."
        },
        {
          "name": "data.status / phone_number_id / waba_id",
          "type": "string",
          "description": "Estado técnico e IDs da identidade nova. connected não garante envio disponível; consulte a saúde atual."
        },
        {
          "name": "data.instance_key_preserved",
          "type": "boolean",
          "description": "true confirma que a chave lógica da instância foi preservada."
        },
        {
          "name": "data.replacement",
          "type": "object",
          "description": "Retrato da identidade anterior, com o mesmo formato de cada item de connection_history. Não é a identidade de envio atual."
        },
        {
          "name": "data.replacement.id / status / subtype / reason",
          "type": "UUID / string",
          "description": "ID da substituição, status=superseded e subtype=meta_identity_replacement. reason=automatic_same_phone_identity_replacement identifica atualização automática dos IDs do mesmo número; explicit_meta_identity_replacement identifica o modo avançado legado."
        },
        {
          "name": "data.replacement.connection_generation / successor_generation",
          "type": "UUID",
          "description": "Gerações anterior e sucessora. A sucessora corresponde à connection_generation na raiz do evento."
        },
        {
          "name": "data.replacement.phone_number_id / waba_id / business_id / display_phone_number / verified_name",
          "type": "string ou null",
          "description": "IDs e rótulos da identidade anterior; nunca use o telefone exibido como prova de substituição."
        },
        {
          "name": "data.replacement.connected_at / superseded_at",
          "type": "data ISO 8601 ou null / data ISO 8601",
          "description": "Momento confirmado do vínculo anterior, quando disponível, e momento da substituição. connected_at não é a primeira conexão de toda a vida do número."
        }
      ],
      "actions": [
        "Deduplicate pelo event_id e reconcilie a mesma instância no GET local; atualize seus IDs Meta atuais somente após conferir a geração.",
        "Exiba a identidade anterior em Substituídos, com o subtipo no card e sem ações de envio, reconexão ou desconexão. Não some esse histórico à capacidade nem aos números ativos.",
        "Se o evento chegar depois de outra reconexão, não restaure a geração antiga. O GET é a autoridade para a identidade atual. Não reinicie o fluxo Embedded automaticamente."
      ],
      "reconcile": [
        "get-partner-instance",
        "list-partner-instances",
        "create-connect-session"
      ]
    },
    {
      "id": "instance.meta_link_removed",
      "title": "Vínculo removido na Meta",
      "summary": "Confirma perda do vínculo da identidade atual, desabilita a autorização local e informa a liberação da vaga sem inventar uma limpeza remota.",
      "when": "Após evento account_update assinado e validado de remoção de parceiro, desinstalação do app ou exclusão da WABA, com escopo do app/parceiro e da WABA/número e data compatíveis com a geração atual. Erro Graph de objeto inacessível ou token inválido não produz este evento. Na coexistência, PARTNER_REMOVED confirmado para a solicitação de desconexão encerra a espera pelo titular: o campo operation pode desaparecer do GET porque foi arquivado. Confirme meta_link.source=signed_meta_webhook e connection_generation correspondente, status=canceled, occupies_slot=false e slot_state=released. Não espere remote_cleanup_confirmed=true: o evento confirma a desvinculação no aplicativo, não a execução de /deregister.",
      "identity": "Envelope event/data com instance_key e connection_generation na raiz. data.meta_link_state contém o vínculo confirmado; o GET da instância o apresenta resumido no campo meta_link.",
      "example": {
        "id": "44444444-4444-4444-8444-444444444444",
        "event_id": "44444444-4444-4444-8444-444444444444",
        "schema_version": "partner-instance-v2",
        "created_at": "2026-10-08T15:00:00Z",
        "partner_id": "22222222-2222-4222-8222-222222222222",
        "customer_id": "11111111-1111-4111-8111-111111111111",
        "external_customer_id": "cliente-001",
        "instance_id": "33333333-3333-4333-8333-333333333333",
        "instance_key": "hc_inst_exemplo",
        "event": "instance.meta_link_removed",
        "connection_generation": "55555555-5555-4555-8555-555555555555",
        "data": {
          "status": "canceled",
          "meta_link_state": {
            "state": "removed",
            "reason": "app_uninstalled",
            "confirmed_at": "2026-10-08T14:50:00Z",
            "connection_generation": "55555555-5555-4555-8555-555555555555",
            "evidence_source": "signed_meta_webhook",
            "source_event_id": "88888888-8888-4888-8888-888888888888",
            "meta_waba_id": "100000000000002",
            "meta_phone_number_id": "100000000000001",
            "app_id": "100000000000005",
            "provider_business_id": null
          },
          "occupies_slot": false,
          "slot_state": "released",
          "capacity_release_reason": "signed_meta_link_removal",
          "local_authorization": "disabled",
          "remote_authorization": "unknown",
          "remote_callback_status": "unavailable",
          "remote_cleanup_confirmed": false,
          "reconnect_required": true
        }
      },
      "fields": [
        {
          "name": "id / event_id / event / schema_version",
          "type": "UUID / string",
          "description": "Identificador estável para deduplicar; event=instance.meta_link_removed e schema_version=partner-instance-v2."
        },
        {
          "name": "partner_id / customer_id / external_customer_id / instance_id / instance_key / connection_generation",
          "type": "UUID / string / null",
          "description": "Identificam a instância, seu cliente e a geração encerrada. Um evento antigo não deve desconectar uma geração sucessora."
        },
        {
          "name": "data.status / meta_link_state.state / meta_link_state.reason",
          "type": "string",
          "description": "canceled, removed e um motivo confirmado: partner_removed, app_uninstalled ou waba_deleted. Pagamento ou RED não são motivos de remoção."
        },
        {
          "name": "data.meta_link_state.confirmed_at / connection_generation / evidence_source",
          "type": "data / UUID / string",
          "description": "Data do fato Meta, geração correspondente e signed_meta_webhook. No GET, a projeção usa meta_link.source, sem expor todo o registro interno."
        },
        {
          "name": "data.meta_link_state.source_event_id / meta_waba_id / meta_phone_number_id / app_id / provider_business_id",
          "type": "UUID / string / null",
          "description": "Referências da evidência assinada e do escopo validado. provider_business_id pode ser null; sua ausência neste caso não confirma perda de outro portfólio."
        },
        {
          "name": "data.occupies_slot / slot_state / capacity_release_reason",
          "type": "boolean / string",
          "description": "false, released e signed_meta_link_removal indicam a liberação local da vaga daquele vínculo, sem garantir a disponibilidade atual após operações posteriores."
        },
        {
          "name": "data.local_authorization / remote_authorization / remote_callback_status / remote_cleanup_confirmed",
          "type": "string / boolean",
          "description": "disabled, unknown, unavailable e false. A HookCloud não executou nem comprovou remoção do callback ou revogação do token global da WABA por esse evento."
        },
        {
          "name": "data.reconnect_required",
          "type": "boolean",
          "description": "true: retornar à operação exige nova jornada autorizada, com cobrança, risco e capacidade verificados."
        }
      ],
      "actions": [
        "Na geração correspondente, apresente Desconectado — Vínculo removido na Meta, desative os sinais de envio e preserve os dados antigos como histórico. O sininho da HookCloud também recebe o aviso.",
        "Deduplicate e reconcilie por get-partner-instance. Não dispare remoção de callback, deregistro, nova conexão ou novo envio automaticamente em resposta ao evento.",
        "Se o GET mostrar meta_link.state=access_unconfirmed, apresente a falta de confirmação com cautela. Não converta falha de acesso em remoção, banimento ou liberação de vaga por conta própria.",
        "Quando o titular concluir no aplicativo, use instance.meta_link_removed para atualizar o cache e o painel. O HTTP 202 anterior tinha disconnect_status=awaiting_user, action_required=disconnect_in_whatsapp_business_app e meta_disconnect.status=action_required; status ainda refletia a conexão real. Não envie novas chamadas de desconexão para tentar acelerar a confirmação."
      ],
      "reconcile": [
        "get-partner-instance",
        "list-partner-instances",
        "create-connect-session"
      ]
    }
  ]
}
