openapi: 3.1.0
info:
  title: HookCloud Partner API
  version: 1.1.0
  description: API pública para clientes, instâncias, callbacks, templates e saúde de entregas. O endpoint é monolítico e
    seleciona a operação por route. Rotas de notificação usam JWT de membro.
servers:
  - url: https://api.hookcloud.app
    description: Produção — operações reais
  - url: https://api-stage.hookcloud.app
    description: Homologação — banco e cobrança de teste separados
tags:
  - name: Partner API
  - name: Partner Portal
paths:
  /functions/v1/swift-worker:
    get:
      operationId: hookcloudGetRoute
      summary: Consultas da Partner API
      description: Use route na query e os parâmetros específicos da operação. Consulte x-hookcloud-route-catalog e o site de
        documentação.
      parameters:
        - in: query
          name: route
          required: true
          schema:
            type: string
            enum:
              - list-partner-customers
              - get-partner-customer
              - get-customer-offboarding
              - get-partner-customer-history
              - get-partner-instance
              - get-partner-instance-history
              - get-partner-webhook-migration
              - list-partner-meta-templates
              - get-partner-meta-template
              - get-partner-meta-template-sync
              - get-partner-delivery-health
              - search-partner-delivery-events
              - list-partner-notifications
              - get-partner-notification-preferences
              - list-partner-instances
              - get-partner-onboarding-status
              - get-partner-webhook-endpoint
              - get-authenticated-webhook-service
              - get-authenticated-webhook-usage
              - get-authenticated-webhook-billing
        - in: header
          name: apikey
          required: true
          schema:
            type: string
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
        - in: query
          name: page_size
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - in: query
          name: search
          schema:
            type: string
        - in: query
          name: customer_id
          schema:
            type: string
            format: uuid
        - in: query
          name: external_customer_id
          schema:
            type: string
        - in: query
          name: job_id
          schema:
            type: string
            format: uuid
          description: Solicitação de encerramento pertencente ao mesmo cliente selecionado.
        - in: query
          name: instance_id
          schema:
            type: string
            format: uuid
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: language
          schema:
            type: string
        - in: query
          name: template_name
          schema:
            type: string
        - in: query
          name: template_id
          schema:
            type: string
        - in: query
          name: meta_waba_id
          schema:
            type: string
        - in: query
          name: window_hours
          schema:
            type: integer
            minimum: 1
            maximum: 2160
            default: 24
        - in: query
          name: date_from
          schema: &a1
            oneOf:
              - type: string
                format: date
              - type: string
                format: date-time
            description: Data calendário YYYY-MM-DD ou ISO 8601 com timezone. Datas impossíveis, ausência de fuso em horários e
              intervalo invertido retornam 400.
        - in: query
          name: date_to
          schema: *a1
        - in: query
          name: partner_id
          schema:
            type: string
            format: uuid
            description: API Key não pode selecionar outro parceiro (403 partner_scope_mismatch).
        - in: query
          name: waba_id
          schema:
            type: string
            description: Alias de meta_waba_id.
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: "Consumo autenticado: primeiro dia UTC inclusivo; intervalo total de até 31 dias."
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: "Consumo autenticado: último dia UTC inclusivo, sem data futura."
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 500
          description: "Entrega autenticada: até 100 integrações na consulta do serviço ou até 500 linhas de consumo."
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
            maximum: 100000
      security:
        - partnerBearer: []
        - memberBearer: []
      responses:
        "200":
          description: Sucesso
        "400":
          description: Parâmetros inválidos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Não autenticado
        "403":
          description: Acesso negado
        "404":
          description: Não encontrado
    post:
      operationId: hookcloudPostRoute
      summary: Comandos da Partner API
      parameters:
        - &a2
          in: header
          name: apikey
          required: true
          schema:
            type: string
        - &a3
          $ref: "#/components/parameters/IdempotencyKey"
      security: &a4
        - partnerBearer: []
        - memberBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              anyOf:
                - $ref: "#/components/schemas/CustomerUpsertRequest"
                - $ref: "#/components/schemas/CreateInstanceRequest"
                - $ref: "#/components/schemas/CreateConnectSessionRequest"
                - $ref: "#/components/schemas/DeactivateInstanceRequest"
                - $ref: "#/components/schemas/DeactivateCustomerRequest"
                - $ref: "#/components/schemas/WebhookEndpointRequest"
                - $ref: "#/components/schemas/ConfigureInstanceWebhookRequest"
                - $ref: "#/components/schemas/TemplateRefreshRequest"
                - $ref: "#/components/schemas/DeliveryStatusRequest"
                - type: object
                  required:
                    - route
                  properties:
                    route:
                      type: string
                - $ref: "#/components/schemas/AuthenticatedWebhookProvisionAuthenticatedWebhook"
                - $ref: "#/components/schemas/AuthenticatedWebhookActivateAuthenticatedWebhook"
                - $ref: "#/components/schemas/AuthenticatedWebhookDrainAuthenticatedWebhook"
                - $ref: "#/components/schemas/AuthenticatedWebhookReconcileAuthenticatedWebhook"
                - $ref: "#/components/schemas/AuthenticatedWebhookRotateAuthenticatedWebhookSecret"
                - $ref: "#/components/schemas/AuthenticatedWebhookCreateAuthenticatedWebhookCheckout"
                - $ref: "#/components/schemas/AuthenticatedWebhookCancelAuthenticatedWebhookSubscription"
          multipart/form-data:
            schema:
              type: object
              required: [ customer_id, file ]
              properties:
                customer_id:
                  type: string
                  format: uuid
                file:
                  type: string
                  format: binary
                  description: Static JPEG, PNG or WebP; maximum 2097152 bytes, 4096 pixels per side and 16000000 total pixels. Animated
                    images rejected; binary content checked.
                partner_id:
                  type: string
                  format: uuid
                  description: Only for authorized administrative context.
            encoding:
              file:
                contentType: image/jpeg, image/png, image/webp
      responses:
        "200":
          description: Sucesso
        "201":
          description: Criado
        "202":
          description: 'Aceito para processamento ou ação do titular. Em deactivate-partner-instance, disconnect_status=confirming
            permite conferência segura da mesma operação; awaiting_user exige desconexão no WhatsApp Business; status
            preserva connected/estado real. Não confirma remoção nem liberação de capacidade. Consulte
            PartnerCoexistenceDisconnectResponse.'
        "400":
          description: Parâmetros inválidos
        "409":
          description: Conflito de lifecycle/Meta/billing
        "429":
          description: Rate limit
    patch:
      operationId: hookcloudPatchRoute
      summary: Atualizações parciais
      parameters:
        - in: header
          name: apikey
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/IdempotencyKey"
      security:
        - partnerBearer: []
        - memberBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CustomerUpdateRequest"
                - $ref: "#/components/schemas/DeactivateInstanceRequest"
                - $ref: "#/components/schemas/DeactivateCustomerRequest"
                - allOf:
                    - $ref: "#/components/schemas/NotificationPreferences"
                    - type: object
                      properties:
                        route:
                          const: update-partner-notification-preferences
                - type: object
                  required:
                    - route
                    - instance_id
                    - instance_name
                  properties:
                    route:
                      const: update-partner-instance-label
                    instance_id:
                      type: string
                      format: uuid
                    instance_name:
                      type: string
      responses:
        "200":
          description: Atualizado
        "202":
          description: 'Desconexão aguarda confirmação (disconnect_status=confirming) ou ação do titular (awaiting_user). Lote de
            cliente retorna status=cancel_pending e archived=false.'
        "409":
          description: Conflito
    delete:
      operationId: hookcloudDeactivateRoute
      summary: Inativação lógica de cliente ou instância
      description: Alias das rotas de lifecycle; remove a conexão remota com proteção e preserva histórico. Não executa
        exclusão física.
      parameters:
        - in: query
          name: route
          schema:
            type: string
            enum:
              - deactivate-partner-customer
              - deactivate-partner-instance
          description: Pode ser informada aqui ou no JSON; se ambos forem usados, os valores devem coincidir.
        - *a2
        - *a3
      security: *a4
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/DeactivateCustomerRequest"
                - $ref: "#/components/schemas/DeactivateInstanceRequest"
      responses:
        "200":
          description: Lifecycle atualizado
        "202":
          description: 'Desconexão aguarda confirmação (disconnect_status=confirming) ou ação do titular (awaiting_user). Lote de
            cliente retorna status=cancel_pending e archived=false.'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PartnerDisconnectAcceptedResponse'
                  - $ref: '#/components/schemas/PartnerCustomerDisconnectPendingResponse'
        "400":
          description: Parâmetro ou JSON inválido
        "403":
          description: Sem escopo de alteração
        "409":
          description: Falha de remoção remota; não presume capacidade liberada
components:
  securitySchemes:
    partnerBearer:
      type: http
      scheme: bearer
      bearerFormat: hc_partner_live
      description: Partner API Key usada apenas no backend do SaaS.
    memberBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT Supabase do membro logado, usado no Partner Portal.
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema:
        type: string
        minLength: 8
        maxLength: 200
      description: UUID recomendado para operações estruturais.
  schemas:
    PartnerDisconnectAcceptedResponse:
      description: 'Solicitação ainda não concluída. A integração HookCloud pode ser encerrada com callback removido e acesso
        local desativado, preservando autorização compartilhada. Revogação Meta e parceria comercial têm escopos
        distintos e não são presumidas.'
      oneOf:
        - $ref: '#/components/schemas/PartnerCoexistenceDisconnectResponse'
        - $ref: '#/components/schemas/PartnerGrantDisconnectPendingResponse'
    PartnerGrantDisconnectPendingResponse:
      type: object
      description: 'HTTP 202: confirmação pendente da mesma operação. GET local apenas lê; repetir deactivate-partner-instance
        quando safe_to_recheck=true confere etapas intentadas sem repetir suas escritas, mas pode executar pela primeira
        vez etapas posteriores ainda não intentadas após confirmar as anteriores. Não liberar capacidade por timeout.'
      required:
        [
          ok,
          instance_id,
          status,
          disconnect_status,
          connection_generation,
          operation_id,
          code,
          safe_to_recheck,
          occupies_slot,
          slot_state,
          meta_disconnect
        ]
      properties:
        ok: { type: boolean, const: true }
        instance_id: { type: string, format: uuid }
        status: { type: string, description: 'Estado real preservado, por exemplo connected.' }
        disconnect_status: { type: string, const: confirming }
        connection_generation: { type: string, format: uuid }
        operation_id: { type: string, format: uuid }
        code: { type: string, enum: [ grant_disconnect_unconfirmed, integration_disconnect_unconfirmed ] }
        safe_to_recheck: { type: boolean, const: true }
        occupies_slot: { type: boolean, const: true }
        slot_state: { type: string, enum: [ counting, reserved ] }
        meta_disconnect:
          type: object
          required: [ status, code, safe_to_recheck, remote_cleanup_confirmed, capacity_retained ]
          properties:
            status: { type: string, const: pending }
            code: { type: string, enum: [ grant_disconnect_unconfirmed, integration_disconnect_unconfirmed ] }
            safe_to_recheck: { type: boolean, const: true }
            remote_cleanup_confirmed: { type: boolean, const: false }
            capacity_retained: { type: boolean, const: true }
    PartnerDisconnectCleanup:
      type: [ object, 'null' ]
      description: 'Projeção pública de prova persistida. hookcloud_integration exige callback removido e acesso local
        desativado; app_authorization exige revogação comprovada. completed exige geração/operação/identidade, canceled
        e released. Não prova deregistro nem remoção da parceria comercial. null não confirma ausência ou conclusão.'
      properties:
        status: { type: string, enum: [ completed, pending ] }
        scope: { type: string, enum: [ app_authorization, hookcloud_integration ] }
        local_authorization: { type: string, enum: [ disabled, disconnect_pending ] }
        remote_authorization:
          {
            type: string,
            enum: [ preserved_shared, unknown ],
            description: 'Em hookcloud_integration, não significa revogação global.'
          }
        proof_source:
          {
            type: string,
            enum: [ grant_revoked_readback, phone_callback_removed_local_access_disabled ],
            description: 'Presente somente em completed, com o escopo correspondente. Não confundir desativação local com revogação
              Meta.'
          }
        connection_generation: { type: string, format: uuid }
        operation_id: { type: string, format: uuid }
        confirmed_at: { type: [ string, 'null' ], format: date-time, description: 'Data da conclusão confirmada; null em pending.' }
        safe_to_recheck:
          {
            type: boolean,
            description: 'true em pending: permite conferir a mesma operação pelo POST deactivate-partner-instance.'
          }
        callback:
          type: object
          properties:
            status: { type: string, enum: [ removed, unconfirmed ] }
        token_revocation:
          type: object
          properties:
            status: { type: string, enum: [ confirmed_invalid, local_disabled, unconfirmed ] }
            remote_status: { type: string, enum: [ preserved_shared, unconfirmed ] }
        commercial_relationship:
          type: object
          properties:
            status: { type: string, const: unchanged }
    PartnerCoexistenceDisconnectResponse:
      type: object
      description: 'HTTP 202 de deactivate-partner-instance. Solicitação legada ainda pendente; siga as instruções atuais.
        resume_hookcloud_integration permite retomada pelo sistema sem exigir celular. Não repita escritas nem libere a
        vaga pelo tempo decorrido. Se safe_to_recheck=true, siga recheck_mode. resume_hookcloud_integration retoma a
        remoção da rota e a desativação local, preservando grants compartilhados; read_only confere intenção existente
        sem novo DELETE; retry_safe_preflight reavalia recusa anterior à intenção e pode iniciar o desligamento. Após
        instance.meta_link_removed, reconcilie o GET local pela geração.'
      required:
        [
          ok,
          instance_id,
          status,
          disconnect_status,
          connection_generation,
          operation_id,
          action_required,
          code,
          occupies_slot,
          slot_state,
          meta_disconnect
        ]
      properties:
        ok: { type: boolean, const: true }
        instance_id: { type: string, format: uuid }
        status:
          {
            type: string,
            description: 'Estado real preservado, por exemplo connected. Não é action_required.',
            examples: [ connected ]
          }
        disconnect_status: { type: string, const: awaiting_user }
        connection_generation: { type: string, format: uuid }
        operation_id: { type: string, format: uuid }
        action_required: { type: string, const: disconnect_in_whatsapp_business_app }
        code: { type: string, enum: [ coexistence_disconnect_in_app_required, grant_disconnect_in_app_required ] }
        safe_to_recheck:
          {
            type: boolean,
            description: 'Quando true, siga recheck_mode na mesma operação/geração; não significa sempre consulta somente de
              leitura.'
          }
        recheck_mode:
          {
            type: string,
            enum: [ read_only, retry_safe_preflight, resume_hookcloud_integration ],
            description: 'resume_hookcloud_integration retoma a rota e desativação local sem exigir celular. read_only consulta
              intenção existente sem repetir DELETE. retry_safe_preflight reavalia recusa anterior à intenção e pode
              iniciar as primeiras escritas autorizadas.'
          }
        message: { type: string }
        instructions: { type: array, items: { type: string } }
        occupies_slot: { type: boolean, description: 'Capacidade atual preservada; não é liberada pelo HTTP 202.', examples: [ true ] }
        slot_state: { type: string, examples: [ counting ] }
        meta_disconnect:
          type: object
          required: [ status, operation_id, connection_generation, action_required ]
          properties:
            status: { type: string, const: action_required }
            operation_id: { type: string, format: uuid }
            connection_generation: { type: string, format: uuid }
            action_required: { type: string, const: disconnect_in_whatsapp_business_app }
    PartnerCustomerOffboardingSummary:
      type: object
      description: 'Resumo durável do pedido explícito de encerramento. technical conta instâncias e commercial conta relações
        entre portfólios. Nunca recebe IDs Meta ou permissões fornecidas pelo consumidor da API.'
      required:
        [
          job_id,
          partner_id,
          customer_id,
          generation,
          status,
          phase,
          customer_status,
          archived,
          technical,
          commercial,
          technical_complete,
          commercial_complete,
          all_relationships_removed,
          issues
        ]
      properties:
        job_id: { type: string, format: uuid }
        partner_id: { type: string, format: uuid }
        customer_id: { type: string, format: uuid }
        generation: { type: string, format: uuid }
        status: { type: string, enum: [ requested, processing, pending, action_required, completed ] }
        phase: { type: string, enum: [ technical, commercial, completed ] }
        customer_status: { type: string, enum: [ cancel_pending, canceled ] }
        archived: { type: boolean }
        technical_complete: { type: boolean }
        commercial_complete: { type: boolean }
        all_relationships_removed:
          {
            type: boolean,
            description: 'Só true se todas as relações identificadas tiverem ausência confirmada. Relação preservada por
              compartilhamento não conta como removida.'
          }
        safe_to_continue:
          {
            type: boolean,
            description: 'Continuação da mesma solicitação. Não autoriza repetir escritas remotas ambíguas nem contornar uma ação
              necessária.'
          }
        processed_items:
          {
            type: integer,
            minimum: 0,
            description: 'Itens internos processados nesta rodada; grupos não equivalem à contagem de números.'
          }
        updated_at: { type: string, format: date-time }
        technical:
          type: object
          required: [ total, confirmed, pending, action_required ]
          properties:
            total: { type: integer, minimum: 0 }
            confirmed: { type: integer, minimum: 0 }
            pending: { type: integer, minimum: 0 }
            action_required: { type: integer, minimum: 0 }
        commercial:
          type: object
          required: [ total, removed, preserved_shared, not_applicable, pending, action_required ]
          properties:
            total: { type: integer, minimum: 0 }
            removed: { type: integer, minimum: 0 }
            preserved_shared:
              {
                type: integer,
                minimum: 0,
                description: 'Resultado terminal correto quando outra conexão, produto ou ativo depende da relação.'
              }
            not_applicable: { type: integer, minimum: 0 }
            pending: { type: integer, minimum: 0 }
            action_required: { type: integer, minimum: 0 }
        issues:
          type: array
          maxItems: 50
          items:
            type: object
            required: [ code, message, scope ]
            properties:
              code:
                {
                  type: string,
                  description: 'Motivo estável. customer_cleanup_scope_limit indica grupo sem conferência inicial concluída em até 40
                    leituras Meta ou 45 segundos; exige avaliação da HookCloud e não entra em repetição automática.'
                }
              message:
                {
                  type: string,
                  description: 'Explicação legível sem mensagem bruta da Meta, credenciais ou informações de outros clientes.'
                }
              scope: { type: string, enum: [ technical, commercial, customer ] }
              operation_id: { type: string, format: uuid }
              instance_id: { type: string, format: uuid }
              client_business_id: { type: string }
        issues_truncated: { type: boolean }
    PartnerCustomerOffboardingResponse:
      type: object
      description: 'POST 200 quando concluído ou 202 enquanto não concluído. GET 200 consulta exclusivamente o registro local;
        não chama a Meta nem inicia etapas.'
      required: [ ok, partner_id, partner_customer_id, external_customer_id, status, archived, offboarding ]
      properties:
        ok: { type: boolean, const: true }
        partner_id: { type: string, format: uuid }
        partner_customer_id: { type: string, format: uuid }
        external_customer_id: { type: string }
        status: { type: string, enum: [ cancel_pending, canceled ] }
        archived: { type: boolean }
        offboarding:
          $ref: '#/components/schemas/PartnerCustomerOffboardingSummary'
        used_slots: { type: integer, minimum: 0 }
        slot_limit: { type: integer, minimum: 0 }
    PartnerCustomerDisconnectPendingResponse:
      description: 'HTTP 202 do encerramento durável do cliente. Não arquiva enquanto faltar prova técnica ou resultado
        comercial terminal; preservação compartilhada é terminal, ambiguidade não.'
      allOf:
        - $ref: '#/components/schemas/PartnerCustomerOffboardingResponse'
        - type: object
          properties:
            status: { type: string, const: cancel_pending }
            archived: { type: boolean, const: false }
    PartnerDisconnectAction:
      type: [ object, 'null' ]
      description: 'Projeção local da ação exigida para desconexão de coexistência. Não é revisão técnica nem comprovação de
        remoção. Após confirmação assinada, fica null e a operação atual pode ser arquivada.'
      properties:
        code: { type: string, const: disconnect_in_whatsapp_business_app }
        status: { type: string, const: awaiting_user }
        message: { type: string }
        instructions: { type: array, items: { type: string } }
        requested_at: { type: string, format: date-time }
        capacity_retained: { type: boolean, const: true }
        reference_url: { type: string, format: uri }
        safe_to_recheck: { type: boolean, description: 'Permite ação explícita na mesma solicitação; leia recheck_mode.' }
        recheck_mode:
          {
            type: string,
            enum: [ read_only, retry_safe_preflight, resume_hookcloud_integration ],
            description: 'read_only consulta intenção existente; retry_safe_preflight reavalia recusa anterior à intenção e pode
              iniciar o desligamento seguro.'
          }
        operation_id: { type: string, format: uuid }
        connection_generation: { type: string, format: uuid }
    PartnerOnboardingStatusResponse:
      type: object
      readOnly: true
      required:
        [
          ok,
          partner_id,
          checkout_confirmed,
          access_basis,
          registration_required,
          registration_complete,
          can_configure_webhook,
          webhook_required,
          webhook_validated,
          can_create_customer,
          can_create_instance,
          webhook
        ]
      description: 'Consulta local, sem chamadas à Meta ou Stripe. Não exponha segredos. Novos cadastros devem permanecer
        bloqueados em falha ou ausência de resposta; cada escrita verifica a autorização novamente.'
      properties:
        ok: { type: boolean, const: true }
        partner_id: { type: string, format: uuid }
        checkout_confirmed:
          type: boolean
          description: 'Confirmação comercial registrada pelo servidor; considera contas internas já habilitadas e não é inferida
            da URL de retorno do checkout.'
        access_basis: { type: string, enum: [ internal_lifetime, stripe ] }
        registration_required: { type: boolean, const: true }
        registration_complete:
          type: boolean
          description: 'Cadastro CPF/CNPJ obrigatório concluído pelo proprietário no Portal. Validação local de formato e dígitos
            verificadores; não comprova identidade, titularidade ou situação ativa na Receita.'
        can_configure_webhook:
          type: boolean
          description: 'Liberação para configurar o webhook global após concluir o cadastro obrigatório. Uma API Key não pode
            concluir o cadastro.'
        webhook_required: { type: boolean, const: true }
        webhook_validated:
          type: boolean
          description: 'GET validado e salvo para a URL atual, habilitada e primária. Não comprova POST.'
        can_create_customer: { type: boolean }
        can_create_instance: { type: boolean }
        webhook:
          type: object
          properties:
            url: { type: [ string, 'null' ], format: uri }
            status: { type: string, enum: [ verified, verification_required, not_configured ] }
            required: { type: boolean, const: true }
            configured: { type: boolean }
            ready: { type: boolean }
            verified_at: { type: [ string, 'null' ], format: date-time }
            verification_method: { type: string, const: GET }
            verification_challenge: { type: string, const: '1234' }
            post_delivery_verified: { type: boolean, const: false }
            next_action: { type: [ string, 'null' ], enum: [ configure_and_save_verified_webhook, null ] }
            message: { type: string }
    PartnerRegistrationRequired:
      description: 'HTTP 409 partner_registration_required nas rotas operacionais enquanto o proprietário não concluir os
        dados obrigatórios no Portal. PF: nome completo, celular e CPF. PJ: razão social, CNPJ, nome completo do
        responsável e celular. Não envie documentos aos endpoints de clientes nem use retry automático; consulte
        get-partner-onboarding-status.'
      allOf:
        - $ref: '#/components/schemas/ErrorEnvelope'
    PartnerWebhookVerificationRequired:
      description: 'HTTP 409 antes de novo cliente, instância ou sessão quando falta GET validado e salvo. Faça
        update-partner-webhook-endpoint e consulte o status; testar sem salvar não libera.'
      allOf:
        - $ref: '#/components/schemas/ErrorEnvelope'
    PartnerMetaLink:
      type: object
      readOnly: true
      required: [ state, reason, confirmed_at, source ]
      description: 'Projeção segura em meta_link e connection_health.meta_link. removed exige evento Meta assinado da
        identidade/geração atual; erro Graph isolado não prova remoção.'
      properties:
        state: { type: string, enum: [ removed, access_unconfirmed, unknown ] }
        reason:
          {
            type: [ string, 'null' ],
            enum: [ partner_removed, app_uninstalled, waba_deleted, permission_error, object_unavailable, null ]
          }
        confirmed_at: { type: [ string, 'null' ], format: date-time }
        source: { type: [ string, 'null' ], enum: [ signed_meta_webhook, graph, null ] }
        connection_generation: { type: string, format: uuid, description: 'Presente para removed; geração confirmada, nunca contador ordenável.' }
        checked_at:
          {
            type: [ string, 'null' ],
            format: date-time,
            description: 'Observação Graph de access_unconfirmed; não é confirmação de remoção.'
          }
    PartnerConnectionHistoryEntry:
      type: object
      readOnly: true
      required:
        [
          id,
          status,
          subtype,
          connection_generation,
          successor_generation,
          phone_number_id,
          waba_id,
          business_id,
          display_phone_number,
          verified_name,
          connected_at,
          superseded_at,
          reason
        ]
      properties:
        id: { type: string, format: uuid }
        status: { type: string, const: superseded }
        subtype: { type: string, const: meta_identity_replacement }
        connection_generation: { type: string, format: uuid, description: 'Geração da identidade anterior.' }
        successor_generation: { type: string, format: uuid }
        phone_number_id: { type: [ string, 'null' ], pattern: '^[0-9]{5,30}$' }
        waba_id: { type: [ string, 'null' ], pattern: '^[0-9]{5,30}$' }
        business_id: { type: [ string, 'null' ], pattern: '^[0-9]{5,30}$' }
        display_phone_number: { type: [ string, 'null' ], maxLength: 60 }
        verified_name: { type: [ string, 'null' ], maxLength: 300 }
        connected_at: { type: [ string, 'null' ], format: date-time }
        superseded_at: { type: string, format: date-time }
        reason: { type: string, const: explicit_meta_identity_replacement }
    PartnerInstanceLifecycleFields:
      type: object
      description: 'Campos aditivos do GET e de cada item da listagem; histórico não duplica a instância ativa nem ocupa
        capacidade.'
      properties:
        disconnect_cleanup:
          $ref: '#/components/schemas/PartnerDisconnectCleanup'
        disconnect_action:
          $ref: '#/components/schemas/PartnerDisconnectAction'
        meta_link:
          $ref: '#/components/schemas/PartnerMetaLink'
        connection_history:
          type: array
          readOnly: true
          maxItems: 50
          description: 'Somente identidades explicitamente substituídas após conclusão. Projeção pública limitada; auditoria
            completa é preservada no servidor.'
          items:
            $ref: '#/components/schemas/PartnerConnectionHistoryEntry'
    PartnerInstanceReadResponse:
      description: 'Campos aditivos da resposta de get-partner-instance; os demais campos existentes são preservados.'
      allOf:
        - $ref: '#/components/schemas/PartnerInstanceLifecycleFields'
        - type: object
          properties:
            whatsapp_manager_url:
              type: [ string, 'null' ]
              format: uri
              readOnly: true
              description: >-
                URL do WhatsApp Manager na raiz da resposta. Use como href do botão "Abrir WhatsApp Manager" no painel
                do assinante e abra no navegador. O destino é montado com os IDs Meta disponíveis; retorna null quando
                business_id, waba_id e phone_number_id estão todos ausentes. Nesse caso, oculte ou desabilite o botão. O
                usuário precisa entrar na Meta e ter permissão no negócio/WABA. O link não contém tokens nem concede
                acesso por si só; nunca acrescente Partner API Key ou token Meta. Sua presença não confirma conexão
                ativa ou envio disponível. Obtenha o campo por get-partner-instance no backend, com as credenciais da
                Partner API mantidas no servidor. A leitura usa os dados armazenados na HookCloud e não exige
                refresh-partner-instance-meta nem consulta à Graph API apenas para obter o link.
              examples:
                - 'https://business.facebook.com/wa/manage/phone-numbers/?business_id=100000000000003&waba_id=100000000000002&phone_number_id=100000000000001'
                - null
    PartnerReplacementTarget:
      type: object
      required: [ meta_waba_id, meta_phone_number_id ]
      description: 'IDs Meta exatos do destino autorizado; seleção Embedded deve corresponder. Não usar telefone exibido nem BSUID.'
      properties:
        meta_waba_id: { type: string, pattern: '^[0-9]{5,30}$' }
        meta_phone_number_id: { type: string, pattern: '^[0-9]{5,30}$' }
        meta_business_id: { type: [ string, 'null' ], pattern: '^[0-9]{5,30}$' }
    PartnerIdentityReplacedEvent:
      type: object
      required:
        [
          id,
          event_id,
          event,
          schema_version,
          created_at,
          partner_id,
          customer_id,
          external_customer_id,
          instance_id,
          instance_key,
          operation_id,
          connection_generation,
          data
        ]
      properties:
        id: { type: string, format: uuid }
        event_id: { type: string, format: uuid }
        event: { type: string, const: instance.identity_replaced }
        schema_version: { type: string, const: partner-instance-v2 }
        created_at: { type: string, format: date-time }
        partner_id: { type: string, format: uuid }
        customer_id: { type: [ string, 'null' ], format: uuid }
        external_customer_id: { type: [ string, 'null' ] }
        instance_id: { type: string, format: uuid }
        instance_key: { type: string }
        operation_id: { type: string, format: uuid }
        connection_generation: { type: string, format: uuid, description: 'Geração sucessora.' }
        data:
          type: object
          required: [ status, replacement, phone_number_id, waba_id, instance_key_preserved ]
          properties:
            status: { type: string, description: 'Estado técnico atual; connected não garante envio liberado.' }
            replacement:
              $ref: '#/components/schemas/PartnerConnectionHistoryEntry'
            phone_number_id: { type: string }
            waba_id: { type: string }
            instance_key_preserved: { type: boolean, const: true }
    PartnerMetaLinkRemovalEvidence:
      type: object
      required:
        [
          state,
          reason,
          confirmed_at,
          connection_generation,
          evidence_source,
          source_event_id,
          meta_waba_id,
          meta_phone_number_id,
          app_id,
          provider_business_id
        ]
      description: 'Evidência no webhook data.meta_link_state. O GET usa o resumo meta_link com source, não evidence_source.'
      properties:
        state: { type: string, const: removed }
        reason: { type: string, enum: [ partner_removed, app_uninstalled, waba_deleted ] }
        confirmed_at: { type: string, format: date-time }
        connection_generation: { type: string, format: uuid }
        evidence_source: { type: string, const: signed_meta_webhook }
        source_event_id: { type: string, format: uuid }
        meta_waba_id: { type: string }
        meta_phone_number_id: { type: [ string, 'null' ] }
        app_id: { type: string }
        provider_business_id: { type: [ string, 'null' ] }
    PartnerMetaLinkRemovedEvent:
      type: object
      required:
        [
          id,
          event_id,
          event,
          schema_version,
          created_at,
          partner_id,
          customer_id,
          external_customer_id,
          instance_id,
          instance_key,
          connection_generation,
          data
        ]
      properties:
        id: { type: string, format: uuid }
        event_id: { type: string, format: uuid }
        event: { type: string, const: instance.meta_link_removed }
        schema_version: { type: string, const: partner-instance-v2 }
        created_at: { type: string, format: date-time }
        partner_id: { type: string, format: uuid }
        customer_id: { type: [ string, 'null' ], format: uuid }
        external_customer_id: { type: [ string, 'null' ] }
        instance_id: { type: string, format: uuid }
        instance_key: { type: string }
        connection_generation: { type: string, format: uuid }
        data:
          type: object
          required:
            [
              status,
              meta_link_state,
              occupies_slot,
              slot_state,
              capacity_release_reason,
              local_authorization,
              remote_authorization,
              remote_callback_status,
              remote_cleanup_confirmed,
              reconnect_required
            ]
          properties:
            status: { type: string, const: canceled }
            meta_link_state:
              $ref: '#/components/schemas/PartnerMetaLinkRemovalEvidence'
            occupies_slot: { type: boolean, const: false }
            slot_state: { type: string, const: released }
            capacity_release_reason: { type: string, const: signed_meta_link_removal }
            local_authorization: { type: string, const: disabled }
            remote_authorization: { type: string, const: unknown }
            remote_callback_status: { type: string, const: unavailable }
            remote_cleanup_confirmed: { type: boolean, const: false }
            reconnect_required: { type: boolean, const: true }
    PartnerConnectionHealth:
      type: object
      description: 'Fotografia aditiva. Inventário ativo não confirma envio disponível. Estado legado e ready_to_operate
        permanecem conservadores.'
      properties:
        state: { type: string, enum: [ ready, limited, blocked, unknown ] }
        ready_to_operate: { type: boolean }
        connection_status: { type: string }
        authorization_status: { type: string }
        callback_status: { type: string }
        inventory_state: { type: string, enum: [ active, attention, restricted, inactive, unknown ] }
        meta_account_state: { type: string, enum: [ no_explicit_restriction_observed, restricted, disabled, unknown ] }
        meta_profile:
          $ref: "#/components/schemas/PartnerMetaOptionalProfile"
        messaging_capability:
          type: object
          properties:
            state: { type: string, enum: [ available, limited, blocked, unavailable, unknown ] }
            reason:
              type: [ string, 'null' ]
              description: 'meta_registration_not_confirmed indica registro do número PENDING, PENDING_REGISTRATION ou UNVERIFIED. Sem
                bloqueio, limitação ou restrição real prioritária, state unknown, ready_to_operate false e
                inventory_state attention; status connected é apenas técnico. Não implica BAN.'
        business_verification:
          type: object
          properties:
            status: { type: string, enum: [ verified, not_verified, pending, unknown ] }
            source: { type: [ string, 'null' ] }
            checked_at: { type: [ string, 'null' ], format: date-time }
            observed_at: { type: [ string, 'null' ], format: date-time }
            stale: { type: boolean }
            unavailable_reason: { type: [ string, 'null' ] }
        meta_payment:
          type: object
          description: 'Fonte de pagamento identificada não garante cartão de crédito, saldo ou cobrança futura. Ausência de erro
            não confirma método; nenhuma promessa de last4 ou bandeira.'
          properties:
            method_presence: { type: string, enum: [ identified, not_confirmed, unknown ] }
            status: { type: string, enum: [ confirmed, no_issue_detected, action_required, unknown ] }
            issue_codes: { type: array, items: { type: integer } }
            method_source: { type: [ string, 'null' ], enum: [ graph_funding, null ] }
            unavailable_reason: { type: [ string, 'null' ] }
            source: { type: [ string, 'null' ] }
            checked_at: { type: [ string, 'null' ], format: date-time }
            observed_at: { type: [ string, 'null' ], format: date-time }
            stale: { type: boolean }
        evidence:
          type: object
          properties:
            schema_version: { type: string, const: partner-health-v2 }
            complete: { type: boolean }
            stale: { type: boolean }
            truncated: { type: boolean }
            checked_at: { type: [ string, 'null' ], format: date-time }
            source: { type: [ string, 'null' ] }
        issues:
          type: array
          items:
            type: object
            properties:
              code: { type: string }
              domain: { type: string }
              scope: { type: string }
              severity: { type: string }
              message: { type: string }
              action: { type: string }
        allowed_actions: { type: array, items: { type: string } }
        operation:
          type: [ object, 'null' ]
          description: 'Operação persistida; processing com lease vencida é apresentado como needs_review, salvo intenção de
            revogação validada no ledger: operation.status=confirming e disconnect_cleanup.status=pending permitem
            rechecagem segura sem repetir DELETE. action_required legado com safe_to_recheck=true e
            recheck_mode=resume_hookcloud_integration permite retomar a desconexão HookCloud sem exigir celular. Não é
            conclusão nem revisão técnica. Após offboarding assinado, operation pode ser null; confira meta_link,
            geração e capacidade. Não repetir mutação remota incerta.'
          properties:
            id: { type: string }
            kind: { type: string, enum: [ connect, disconnect, callback_migration, billing_suspend, unknown ] }
            status: { type: string, enum: [ processing, confirming, completed, failed, needs_review, action_required, stale, unknown ] }
            started_at: { type: [ string, 'null' ], format: date-time }
            finished_at: { type: [ string, 'null' ], format: date-time }
            lease_until: { type: [ string, 'null' ], format: date-time }
            result_code: { type: [ string, 'null' ] }
            generation: { type: [ string, 'null' ], format: uuid }
            remote_cleanup_confirmed: { type: boolean }
            capacity_retained: { type: boolean }
            steps: { type: object, description: 'callback/deregister/provision com state confirmed, uncertain ou not_attempted e at.' }
        observation_scope: { type: string, enum: [ current, historical ] }
        disconnect_cleanup:
          $ref: '#/components/schemas/PartnerDisconnectCleanup'
        disconnect_action:
          $ref: '#/components/schemas/PartnerDisconnectAction'
        meta_link:
          $ref: "#/components/schemas/PartnerMetaLink"
        panel_category:
          type: string
          enum:
            [
              active,
              processing,
              verification_pending,
              account_alert,
              restricted,
              banned,
              callback_review,
              attention,
              disconnected,
              superseded,
              transferred,
              terminated
            ]
          description: Categoria de exibição; não concede capacidade nem autorização. Históricos confirmados ficam em
            Substituídos, com subtipos no card; mover cliente ou reconectar a mesma identidade não os produz.
    PartnerMetaOptionalProfile:
      type: object
      description: 'Observações opcionais, independentes da saúde, com cache de até 60 minutos por identidade e geração.
        Informação ausente ou antiga não confirma limite nem username ativo.'
      properties:
        messaging_limit:
          type: object
          properties:
            status: { type: string, enum: [ available, unavailable ] }
            tier:
              {
                type: [ string, 'null' ],
                description: 'Faixa Meta observada, por exemplo TIER_2K ou TIER_UNLIMITED. Não é consumo, saldo nem quantidade de
                  números contratada.'
              }
            scope:
              {
                type: [ string, 'null' ],
                enum: [ business_portfolio, phone_legacy, null ],
                description: 'business_portfolio compartilha a faixa entre os números do portfólio, para usuários únicos fora da janela
                  de atendimento em 24 horas móveis. phone_legacy identifica fallback antigo; não cria capacidade
                  extra.'
              }
            source: { type: [ string, 'null' ], enum: [ graph_phone_portfolio_limit, graph_phone_legacy_limit, null ] }
            checked_at: { type: [ string, 'null' ], format: date-time }
            stale: { type: boolean }
            unavailable_reason:
              {
                type: [ string, 'null' ],
                description: 'Motivo limitado como permission_error, field_unavailable, identity_mismatch, rate_limited, timeout ou
                  stale. Não comprova ausência do dado.'
              }
        username:
          type: object
          properties:
            status:
              {
                type: string,
                enum: [ available, unavailable ],
                description: 'available confirma somente que o texto foi observado. Não confirma aprovação ou ativação.'
              }
            value: { type: [ string, 'null' ], description: 'Username comercial observado. Não é BSUID nem destinatário de mensagens.' }
            lifecycle_status:
              {
                type: [ string, 'null' ],
                enum: [ approved, reserved, null ],
                description: 'reserved é reserva, não uso ativo. null preserva situação não confirmada, mesmo quando value está
                  disponível.'
              }
            source: { type: [ string, 'null' ], enum: [ graph_phone_username, null ] }
            checked_at: { type: [ string, 'null' ], format: date-time }
            stale: { type: boolean }
            unavailable_reason: { type: [ string, 'null' ] }
    PartnerMetaProfileCompatibilityFields:
      type: object
      description: 'Campos aditivos na raiz de get/refresh e em cada items[] da listagem. Aliases são derivados do perfil
        atual; metadata legado sem evidência não cria positivos.'
      properties:
        meta_profile:
          $ref: '#/components/schemas/PartnerMetaOptionalProfile'
        messaging_limit_tier:
          {
            type: [ string, 'null' ],
            description: 'Alias de meta_profile.messaging_limit.tier; apesar do nome, pode ter origem moderna. Leia scope/source
              para distinguir portfolio de legado.'
          }
        business_username: { type: [ string, 'null' ], description: 'Alias de meta_profile.username.value.' }
        business_username_status:
          {
            type: [ string, 'null' ],
            enum: [ approved, reserved, null ],
            description: 'Alias de meta_profile.username.lifecycle_status; nunca inferir ativo apenas pelo texto.'
          }
    PartnerCapacityEvidence:
      type: object
      properties:
        paid_capacity_current_period: { type: [ integer, 'null' ], description: 'Null quando não existe prova de pagamento; trial não é mensalidade paga.' }
        used_slots: { type: integer }
        reserved_slots: { type: integer }
        available_slots: { type: integer }
        renewal_capacity: { type: integer }
        original_contract_started_at: { type: [ string, 'null' ], format: date-time }
        next_billing_at: { type: [ string, 'null' ], format: date-time }
        paid_period: { type: [ object, 'null' ] }
        observed_period: { type: object }
        latest_invoice: { type: [ object, 'null' ] }
        payment_evidence: { type: object }
    ErrorEnvelope:
      type: object
      properties:
        request_id: { type: string, format: uuid }
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string
            request_id: { type: string, format: uuid }
            details: {}
    CustomerEditableFields:
      type: object
      additionalProperties: false
      properties:
        customer_name:
          type: string
          minLength: 2
          maxLength: 200
        customer_email:
          type:
            - string
            - "null"
          format: email
          maxLength: 320
        customer_phone:
          type:
            - string
            - "null"
          minLength: 6
          maxLength: 40
    CustomerUpsertRequest:
      type: object
      required:
        - route
        - external_customer_id
        - customer_name
      properties:
        route:
          const: upsert-partner-customer
        external_customer_id:
          type: string
          minLength: 1
          maxLength: 200
          description: Imutável por cliente.
        customer_name:
          type: string
          minLength: 2
          maxLength: 200
        customer_email:
          type:
            - string
            - "null"
          format: email
        customer_phone:
          type:
            - string
            - "null"
        metadata:
          type: object
          additionalProperties: true
    CustomerUpdateRequest:
      type: object
      required:
        - changes
      properties:
        route:
          const: update-partner-customer
        customer_id:
          type: string
          format: uuid
        changes:
          $ref: "#/components/schemas/CustomerEditableFields"
        expected_updated_at:
          type:
            - string
            - "null"
          format: date-time
        reason:
          type:
            - string
            - "null"
          maxLength: 500
        external_customer_id:
          type: string
          description: Seletor alternativo ao UUID; não altera o identificador do cliente.
      additionalProperties: false
      anyOf:
        - required:
            - customer_id
        - required:
            - external_customer_id
      description: Use changes com os campos editáveis. A implementação também aceita edição plana com os campos na raiz;
        seletores não pertencem ao envelope changes.
    CreateInstanceRequest:
      type: object
      required:
        - route
        - external_customer_id
      properties:
        route:
          const: create-customer-instance
        partner_customer_id:
          type: string
          format: uuid
        external_customer_id:
          type: string
        instance_key:
          type: string
          description: Gerada pelo servidor para novos números. Uma chave já existente é aceita somente para reaproveitar a mesma
            instância; chave nova informada retorna instance_key_server_managed.
        instance_name:
          type:
            - string
            - "null"
        create_connect_session:
          type: boolean
          default: true
        return_url:
          type:
            - string
            - "null"
          format: uri
        cancel_url:
          type:
            - string
            - "null"
          format: uri
        success_url:
          type:
            - string
            - "null"
          format: uri
        error_url:
          type:
            - string
            - "null"
          format: uri
        ttl_hours:
          type:
            - number
            - "null"
          maximum: 168
          not:
            type: number
            exclusiveMinimum: 0
            exclusiveMaximum: 0.016666666666666666
          description: 'Prazo em horas. Um número finito positivo entre 1/60 de hora (1 minuto) e 168 horas (7 dias) define a
            duração. Omissão, null ou valor não positivo usa o padrão configurado, com fallback de 24 horas. Texto,
            número não finito, valor positivo inferior a 1/60 de hora ou acima de 168 retornam 400 invalid_ttl_hours
            antes de criar registros. Use expires_at/connect_expires_at da resposta como prazo exato.'
        metadata:
          type: object
    CreateConnectSessionRequest:
      type: object
      description: 'Link temporário; replace preserva a chave após troca explícita da identidade, com encerramento anterior
        confirmado. Não conclui a conexão.'
      allOf:
        - if:
            required: [ mode ]
            properties:
              mode: { const: replace }
          then:
            required: [ expected_connection_generation, replacement_target ]
      required:
        - route
      properties:
        route:
          const: create-connect-session
        instance_id:
          type: string
          format: uuid
        instance_key:
          type: string
        mode:
          type: string
          enum:
            - connect
            - reconnect
            - replace
          default: connect
        expected_connection_generation:
          type: string
          format: uuid
          description: 'Somente para o modo avançado replace; não necessário para reconexão normal. UUID exato da geração atual
            obtido no GET, não um contador.'
        replacement_target:
          $ref: '#/components/schemas/PartnerReplacementTarget'
        return_url:
          type:
            - string
            - "null"
          format: uri
        cancel_url:
          type:
            - string
            - "null"
          format: uri
        ttl_hours:
          type:
            - number
            - "null"
          maximum: 168
          not:
            type: number
            exclusiveMinimum: 0
            exclusiveMaximum: 0.016666666666666666
          description: 'Prazo em horas. Um número finito positivo entre 1/60 de hora (1 minuto) e 168 horas (7 dias) define a
            duração. Omissão, null ou valor não positivo usa o padrão configurado, com fallback de 24 horas. Texto,
            número não finito, valor positivo inferior a 1/60 de hora ou acima de 168 retornam 400 invalid_ttl_hours
            antes de criar registros. Use expires_at/connect_expires_at da resposta como prazo exato.'
    DeactivateInstanceRequest:
      type: object
      required:
        - route
      properties:
        route:
          const: deactivate-partner-instance
        instance_id:
          type: string
          format: uuid
        instance_key:
          type: string
        reason:
          type:
            - string
            - "null"
    DeactivateCustomerRequest:
      type: object
      required:
        - route
      properties:
        route:
          enum:
            - deactivate-partner-customer
            - reactivate-partner-customer
        partner_customer_id:
          type: string
          format: uuid
        external_customer_id:
          type: string
        reason:
          type:
            - string
            - "null"
    WebhookEndpointRequest:
      type: object
      required:
        - route
        - webhook_url
      properties:
        route:
          enum:
            - validate-partner-webhook-endpoint
            - update-partner-webhook-endpoint
        webhook_url:
          type: string
          format: uri
          pattern: ^https://
        meta_verify_token:
          type:
            - string
            - "null"
        reason:
          type:
            - string
            - "null"
    ConfigureInstanceWebhookRequest:
      type: object
      description: 'Selecione route na query ou neste JSON; se ambos forem usados, devem coincidir. Configura somente a
        instância selecionada e preserva o endpoint global e as demais instâncias.'
      required: [ instance_id, webhook_url, meta_verify_token ]
      properties:
        route:
          type: string
          const: configure-partner-instance-webhook
        partner_id:
          type: string
          format: uuid
          description: 'Opcional para Partner API Key; obrigatório no contexto administrativo. Não permite selecionar outro
            parceiro.'
        instance_id:
          type: string
          format: uuid
          description: 'UUID da instância existente. Não aceita instance_key ou external_customer_id como seletor.'
        webhook_url:
          type: string
          format: uri
          pattern: '^https://'
          description: 'HTTPS público, sem fragmento, credenciais embutidas, host privado ou porta não permitida. A verificação
            GET não segue redirecionamentos. Prepare também o POST.'
        meta_verify_token:
          type: string
          minLength: 1
          maxLength: 512
          pattern: '^[^\u0000-\u0020\u007f]+$'
          writeOnly: true
          description: 'Token aceito no GET hub.mode=subscribe, hub.verify_token e hub.challenge. Armazenado cifrado e omitido da
            resposta. Não autentica POST e não é API Key, token Meta, App Secret ou segredo HMAC.'
      example:
        route: configure-partner-instance-webhook
        instance_id: 33333333-3333-4333-8333-333333333333
        webhook_url: https://integracao.example.com/webhooks/whatsapp
        meta_verify_token: TOKEN_DE_VERIFICACAO_FICTICIO
    InstanceApplicationWebhook:
      type: object
      required: [ scope, url, verified_at, status, applied, applied_at ]
      properties:
        scope:
          type: string
          const: instance
        url:
          type: string
          format: uri
        verified_at:
          type: string
          format: date-time
        configured_at:
          type: string
          format: date-time
        status:
          type: string
          enum: [ configured, active, needs_review ]
          description: 'configured prepara destino futuro; active confirma aplicação à identidade atual; needs_review indica
            resultado remoto não confirmado.'
        applied:
          type: boolean
          description: 'Confirma configuração remota na identidade atual, não entrega ou processamento pelo receptor.'
        applied_at:
          type: [ string, 'null' ]
          format: date-time
    ConfigureInstanceWebhookResponse:
      type: object
      required: [ ok, instance_id, application_webhook ]
      description: 'HTTP 200. Não retorna partner_id na raiz, verify token, App Secret ou segredo de assinatura. Consulte
        get-partner-instance para conferir a identidade, connection_generation e application_webhook.'
      properties:
        ok:
          type: boolean
          const: true
        unchanged:
          type: boolean
          const: true
          description: 'Presente quando a mesma configuração já está confirmada; não representa garantia geral de idempotência.'
        instance_id:
          type: string
          format: uuid
        application_webhook:
          $ref: '#/components/schemas/InstanceApplicationWebhook'
    TemplateRefreshRequest:
      type: object
      required:
        - route
      properties:
        route:
          const: refresh-partner-meta-templates
        instance_id:
          type: string
          format: uuid
        waba_id:
          type: string
        force:
          type: boolean
          default: false
    DeliveryStatusRequest:
      type: object
      required:
        - route
        - phone_number_id
        - status
      properties:
        route:
          enum:
            - report-partner-message-status
            - report-partner-message-delivery-failure
        phone_number_id:
          type: string
        message_id:
          type:
            - string
            - "null"
        client_message_id:
          type:
            - string
            - "null"
        request_id:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - sent
            - delivered
            - read
            - failed
            - deleted
        template_id:
          type:
            - string
            - "null"
        template_name:
          type:
            - string
            - "null"
        campaign_id:
          type:
            - string
            - "null"
        recipient_hash:
          type:
            - string
            - "null"
          pattern: ^[0-9a-fA-F]{64}$
        error_code:
          type:
            - integer
            - string
            - "null"
        error_title:
          type:
            - string
            - "null"
        error_message:
          type:
            - string
            - "null"
        error_details:
          type:
            - string
            - "null"
        occurred_at:
          oneOf:
            - type: string
              format: date-time
            - type: string
              pattern: ^[0-9]+(?:\.[0-9]+)?$
            - type: number
            - type: "null"
          description: ISO 8601 ou timestamp Unix em segundos; valores inválidos retornam 400.
        recipient_user_id:
          type:
            - string
            - "null"
          description: BSUID opaco recebido da Meta. Não converter em número nem remover prefixo.
        user_id:
          type:
            - string
            - "null"
          description: BSUID opaco recebido da Meta. Não converter em número nem remover prefixo.
        recipient:
          type:
            - string
            - "null"
          description: BSUID opaco recebido da Meta. Não converter em número nem remover prefixo.
    NotificationPreferences:
      type: object
      properties:
        notify_academy:
          type: boolean
          default: true
        notify_meta_alerts:
          type: boolean
          default: true
        notify_templates:
          type: boolean
          default: true
        notify_delivery_health:
          type: boolean
          default: true
        notify_billing:
          type: boolean
          default: true
        notify_billing_reminders:
          type: boolean
          default: true
        notify_system:
          type: boolean
          default: true
        popup_enabled:
          type: boolean
          default: true
    InstanceListResponse:
      type: object
      required:
        - ok
        - items
        - total
        - page
        - page_size
        - pages
      properties:
        ok:
          const: true
        partner_id:
          type: string
          format: uuid
        items:
          type: array
          items:
            type: object
            properties:
              first_connected_at:
                type: [ string, "null" ]
                format: date-time
                readOnly: true
                description: Primeira evidência registrada de conexão técnica confirmada para a identidade do número Meta dentro deste
                  parceiro. Null sem evidência suficiente; reconexões e movimentações no mesmo parceiro preservam o
                  registro. Nunca derivado de created_at.
              connection_health:
                $ref: "#/components/schemas/PartnerConnectionHealth"
              inventory_state: { type: string, enum: [ active, attention, restricted, inactive, unknown ] }
              messaging_capability: { type: object }
              business_verification: { type: object }
              meta_payment: { type: object }
              evidence: { type: object }
              issues: { type: array, items: { type: object } }
              id:
                type: string
                format: uuid
              instance_key:
                type: string
              status:
                type: string
              meta_phone_number_id:
                type:
                  - string
                  - "null"
              meta_waba_id:
                type:
                  - string
                  - "null"
              occupies_slot:
                type: boolean
              slot_state:
                type:
                  - string
                  - "null"
        total:
          type: integer
        page:
          type: integer
        page_size:
          type: integer
        pages:
          type: integer
    WebhookEndpointConfigResponse:
      type: object
      properties:
        ok:
          const: true
        partner_id:
          type: string
          format: uuid
        endpoint:
          type:
            - object
            - "null"
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            url:
              type: string
              format: uri
            enabled:
              type: boolean
            is_primary:
              type: boolean
            meta_verify_token_configured:
              type: boolean
      description: "Retorno de configuração sanitizado: sem meta_verify_token ou valores de headers secretos."
    AuthenticatedWebhookProvisionAuthenticatedWebhook:
      type: object
      required:
        - instance_id
      properties:
        route:
          type: string
          const: provision-authenticated-webhook
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        instance_id:
          type: string
          description: Instância com vínculo Meta atual e estado connected ou paused.
          format: uuid
        endpoint_id:
          type: string
          description: Endpoint do mesmo parceiro. Alternativamente informe webhook_url e meta_verify_token sem endpoint_id para
            um destino próprio. Se nenhuma URL for enviada, o backend usa o endpoint cadastrado selecionado/padrão. Não
            combine um endpoint cadastrado com outra URL.
          format: uuid
        webhook_url:
          type: string
          description: URL final do receptor. Se não informada, usa o endpoint autorizado. Deve coincidir com o endpoint
            selecionado quando houver; destinos privados/redirecionamentos/credenciais na URL são recusados.
        meta_verify_token:
          type: string
          description: Token do GET configurado no receptor. Até 512 caracteres, sem espaços ou controles. Pode vir do endpoint
            autorizado quando não informado. Não é o segredo de assinatura.
      description: Valida o destino HTTPS e o GET do receptor, prepara uma integração pendente e apresenta o segredo uma única
        vez. Header Idempotency-Key obrigatório, com UUID por intenção; não repetir mutação ambígua.
    AuthenticatedWebhookActivateAuthenticatedWebhook:
      type: object
      required:
        - integration_id
        - receiver_ready
      properties:
        route:
          type: string
          const: activate-authenticated-webhook
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        integration_id:
          type: string
          description: ID retornado pela preparação, pertencente ao mesmo parceiro. Não é instance_id nem phone_number_id.
          format: uuid
        receiver_ready:
          type: boolean
          description: Envie true somente após configurar e testar o receptor. O campo registra sua confirmação; não instala
            código por você.
          const: true
      description: Aplica o callback autenticado à instância e confirma o vínculo remoto antes de marcar o serviço ativo.
        Header Idempotency-Key obrigatório, com UUID por intenção; não repetir mutação ambígua.
    AuthenticatedWebhookDrainAuthenticatedWebhook:
      type: object
      required:
        - integration_id
      properties:
        route:
          type: string
          const: drain-authenticated-webhook
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        integration_id:
          type: string
          description: ID retornado pela preparação, pertencente ao mesmo parceiro. Não é instance_id nem phone_number_id.
          format: uuid
      description: Solicita encerramento planejado do serviço opcional, restaurando o destino direto confirmado e concluindo
        as entregas anteriores. Header Idempotency-Key obrigatório, com UUID por intenção; não repetir mutação ambígua.
    AuthenticatedWebhookReconcileAuthenticatedWebhook:
      type: object
      required:
        - integration_id
      properties:
        route:
          type: string
          const: reconcile-authenticated-webhook
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        integration_id:
          type: string
          description: ID retornado pela preparação, pertencente ao mesmo parceiro. Não é instance_id nem phone_number_id.
          format: uuid
      description: Confere o resultado de uma operação de callback ambígua e consulta o estado atual da integração e da fila.
        Header Idempotency-Key obrigatório, com UUID por intenção; não repetir mutação ambígua.
    AuthenticatedWebhookRotateAuthenticatedWebhookSecret:
      type: object
      required:
        - integration_id
        - phase
      properties:
        route:
          type: string
          const: rotate-authenticated-webhook-secret
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        integration_id:
          type: string
          description: ID retornado pela preparação, pertencente ao mesmo parceiro. Não é instance_id nem phone_number_id.
          format: uuid
        phase:
          type: string
          description: prepare gera nova chave; confirm confirma a chave já instalada no receptor.
          enum:
            - prepare
            - confirm
        rotation_id:
          type: string
          description: "Obrigatório em confirm: identificador retornado por prepare. Não reutilize o de outra integração."
          format: uuid
      description: Prepara uma nova chave de assinatura e só passa a usá-la depois da sua confirmação. Header Idempotency-Key
        obrigatório, com UUID por intenção; não repetir mutação ambígua.
    AuthenticatedWebhookCreateAuthenticatedWebhookCheckout:
      type: object
      required:
        - terms_accepted
        - terms_version
      properties:
        route:
          type: string
          const: create-authenticated-webhook-checkout
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        terms_accepted:
          type: boolean
          description: true somente depois do aceite dos valores, excedentes e termos pelo owner.
        terms_version:
          type: string
          description: "Versão imutável das condições específicas aceitas:
            https://partner-politica-de-privacidade.hookcloud.app/webhook-autenticado/2026-10-25/."
      description: Cria o checkout seguro da Stripe para a contratação mensal do adicional pelo owner. Header Idempotency-Key
        obrigatório, com UUID por intenção; não repetir mutação ambígua.
    AuthenticatedWebhookCancelAuthenticatedWebhookSubscription:
      type: object
      required:
        - cancel_at_period_end
      properties:
        route:
          type: string
          const: cancel-authenticated-webhook-subscription
          description: Pode ser informado na query route, conforme os exemplos do catálogo.
        cancel_at_period_end:
          type: boolean
          description: true cancela a renovação no fim do ciclo; false mantém a renovação enquanto permitido. Não cancela
            retroativamente o excedente.
      description: Altera apenas a renovação de Webhook Autenticado + Fila, preservando pacotes de números. Header
        Idempotency-Key obrigatório, com UUID por intenção; não repetir mutação ambígua.
x-hookcloud-route-catalog:
  upsert-partner-customer:
    method: POST
    auth: partner_api_key
    summary: Criar ou atualizar cliente
    required:
      - external_customer_id
      - customer_name
  list-partner-customers:
    method: GET
    auth: partner_api_key
    summary: Listar clientes
    query:
      - page
      - page_size
      - search
      - status
      - include_merged
      - include_erased
  get-partner-customer:
    method: GET
    auth: partner_api_key
    summary: Consultar cliente
    query:
      - customer_id|external_customer_id
  update-partner-customer:
    method: PATCH
    auth: partner_api_key
    summary: Editar nome, e-mail e telefone
    required:
      - customer_id
      - changes
  get-partner-customer-history:
    method: GET
    auth: partner_api_key
    summary: Histórico do cliente
  merge-partner-customers:
    method: POST
    auth: partner_api_key
    summary: Mesclar cadastros
    idempotency: true
  move-partner-instance-to-customer:
    method: POST
    auth: partner_api_key
    summary: Mover instância
    idempotency: true
  create-customer-instance:
    method: POST
    auth: partner_api_key
    summary: Criar instância
  create-connect-session:
    method: POST
    auth: partner_api_key
    summary: Gerar Connect Session para conexão, reconexão ou substituição explícita
    request_schema:
      $ref: '#/components/schemas/CreateConnectSessionRequest'
    response_fields: [ ok, instance_id, connect_session_id, connect_url, expires_at ]
    success_status: 201
  get-partner-instance:
    method: GET
    auth: partner_api_key
    summary: Consultar instância
    response_schema:
      $ref: '#/components/schemas/PartnerInstanceReadResponse'
    lifecycle_fields_schema:
      $ref: '#/components/schemas/PartnerInstanceLifecycleFields'
  get-partner-instance-history:
    method: GET
    auth: partner_api_key_or_member_jwt
    scopes: [ instances.read ]
    required: [ instance_id ]
    pagination: { page: 1, page_size: 50, maximum_page_size: 200 }
    summary: Observações de saúde por geração, desde a implantação
    response: { schema_version: partner-health-v2, history_complete: false, coverage: observations_since_deployment }
  refresh-partner-instance-meta:
    method: POST
    auth: partner_api_key
    summary: Atualizar dados Meta
  update-partner-instance-label:
    method: PATCH
    auth: partner_api_key
    summary: Editar apelido interno
  reveal-partner-instance-meta-token:
    method: POST
    auth: partner_api_key
    summary: Revelar token Meta
  deactivate-partner-instance:
    method: POST
    auth: partner_api_key
    summary: 'Desconectar a integração HookCloud, preservando autorizações compartilhadas'
    response_202_schema: '#/components/schemas/PartnerDisconnectAcceptedResponse'
    coexistence_completion_event: instance.meta_link_removed
    methods:
      - POST
      - PATCH
      - DELETE
  get-customer-offboarding:
    method: GET
    auth: partner_api_key_or_member_jwt
    scopes: [ customers.read ]
    summary: Consultar progresso e pendências do encerramento do cliente
    query: [ customer_id|external_customer_id, job_id ]
    meta_get: false
    response_schema: '#/components/schemas/PartnerCustomerOffboardingResponse'
  deactivate-partner-customer:
    method: POST
    auth: partner_api_key
    summary: Encerrar cliente em job durável com retirada comercial segura
    response_202_schema: '#/components/schemas/PartnerCustomerDisconnectPendingResponse'
    methods:
      - POST
      - PATCH
      - DELETE
  reactivate-partner-customer:
    method: POST
    auth: partner_api_key
    summary: Reativar cliente
  validate-partner-webhook-endpoint:
    method: POST
    auth: partner_api_key
    summary: Testar webhook
  update-partner-webhook-endpoint:
    method: POST
    auth: partner_api_key
    summary: Alterar webhook
  configure-partner-instance-webhook:
    method: POST
    methods: [ POST ]
    auth: partner_api_key_or_member_jwt
    scope: webhook.write
    member_roles: [ owner, admin ]
    summary: Definir webhook individual de uma instância
    request_schema: '#/components/schemas/ConfigureInstanceWebhookRequest'
    response_schema: '#/components/schemas/ConfigureInstanceWebhookResponse'
    meta_request: true
    meta_get: false
    prerequisites: 'Acesso operacional e cliente ativo. Instância conectada/pausada, encerrada com vaga liberada ou ainda
      não autorizada sem phone_number_id. Operação pendente ou identidade incerta bloqueia a mudança.'
    description: 'Valida o desafio GET antes de persistir ou aplicar o callback individual. O override é preservado no
      Connect/reconexão e nas migrações globais. Não altera o endpoint primário nem o destino dos demais números.'
    authentication_note: 'Mensagens/status podem chegar diretamente da Meta. X-Hub-Signature-256 usa o App Secret do app
      emissor, que esta rota não fornece. O verify token valida apenas o GET; estabeleça a autenticação POST com a
      HookCloud antes de ativar o receptor. Não existe relay público ou reassinatura implícita para este callback.'
    event_delivery_note: 'Eventos normalizados HookCloud continuam conforme as inscrições do endpoint global; configurar o
      callback individual não migra esses eventos.'
    retry_note: 'Idempotency-Key não constitui contrato de repetição desta rota. Em timeout, processing ou needs_review,
      consulte get-partner-instance e a operação antes de repetir. Preserve o receptor anterior para entregas em
      trânsito.'
    removal_note: 'Esta rota não oferece DELETE ou remoção do override. URL vazia não restaura o global.'
    errors:
      '400': [ invalid_instance_id, invalid_meta_verify_token, webhook_url_fragment_forbidden ]
      '403': [ webhook_management_forbidden ]
      '404': [ instance_not_found ]
      '409':
        [
          instance_operation_in_progress,
          application_webhook_connection_uncertain,
          stale_connection_generation,
          application_webhook_write_conflict,
          application_webhook_remote_unconfirmed
        ]
      '422': [ webhook_verification_failed ]
      '503': [ application_webhook_configuration_failed ]
  get-partner-webhook-migration:
    method: GET
    auth: partner_api_key
    summary: Consultar migração
  retry-partner-webhook-migration:
    method: POST
    auth: partner_api_key
    summary: Reprocessar migração
  list-partner-meta-templates:
    method: GET
    auth: partner_api_key
    summary: Listar templates
    query:
      - quality_status (Meta quality string)
      - is_risky (boolean, omitted means all)
      - page (integer 1..1000000)
      - page_size (integer 1..200)
  get-partner-meta-template:
    method: GET
    auth: partner_api_key
    summary: Consultar template
    query:
      - instance_id|meta_waba_id
      - template_id OR template_name+language
    description: Nome exige idioma; nome e ID não podem ser enviados juntos. Escopo de WABA é exigido quando há várias WABAs.
  refresh-partner-meta-templates:
    method: POST
    auth: partner_api_key
    summary: Atualizar catálogo
  get-partner-meta-template-sync:
    method: GET
    auth: partner_api_key
    summary: Consultar sync
  report-partner-message-status:
    method: POST
    auth: partner_api_key
    summary: Reportar status de entrega
    scope: instances.write
    member_roles:
      - owner
      - admin
      - developer
    forbidden_roles_error: 403 delivery_report_forbidden
  report-partner-message-delivery-failure:
    method: POST
    auth: partner_api_key
    summary: Reportar falha
    scope: instances.write
    member_roles:
      - owner
      - admin
      - developer
    forbidden_roles_error: 403 delivery_report_forbidden
  get-partner-delivery-health:
    method: GET
    auth: partner_api_key
    summary: Saúde de entregas
    scope: meta_risk.read
    query:
      - window_hours (integer 1..2160)
      - customer_id|partner_customer_id (UUID)
      - instance_id|partner_instance_id (UUID)
      - alert_page (integer, starts at 1)
      - alert_page_size (integer 1..200)
      - alert_search (string)
      - alert_status (open|acknowledged|resolved|all)
    description: Alerts respect customer and instance scope; window_hours applies to message metrics, not open incident age.
  search-partner-delivery-events:
    method: GET
    auth: partner_api_key
    summary: Pesquisar eventos
    scope: meta_risk.read
    query:
      - page (integer 1..1000000)
      - page_size (integer 1..200)
      - date_from (date or timezone datetime)
      - date_to (date or timezone datetime)
      - customer_id|partner_customer_id (UUID)
      - instance_id|partner_instance_id (UUID)
  list-partner-notifications:
    method: GET
    auth: member_jwt
    summary: Listar notificações
  get-partner-notification-preferences:
    method: GET
    auth: member_jwt
    summary: Consultar preferências
  update-partner-notification-preferences:
    method: PATCH
    auth: member_jwt
    summary: Atualizar preferências
  mark-partner-notification-read:
    method: POST
    auth: member_jwt
    summary: Marcar como lida
  mark-all-partner-notifications-read:
    method: POST
    auth: member_jwt
    summary: Marcar todas como lidas
  list-partner-instances:
    method: GET
    auth: partner_api_key
    scope: instances.read
    summary: Listar instâncias do próprio parceiro
    query:
      - page
      - page_size
      - customer_id|partner_customer_id|external_customer_id
      - status
    response_schema:
      $ref: "#/components/schemas/InstanceListResponse"
  get-partner-onboarding-status:
    method: GET
    auth: partner_api_key_or_member_jwt
    scope: webhook.read
    member_roles: [ owner, admin, developer ]
    summary: Conferir confirmação comercial e webhook validado antes de novos cadastros
    response_schema:
      $ref: '#/components/schemas/PartnerOnboardingStatusResponse'
  get-partner-webhook-endpoint:
    method: GET
    auth: partner_api_key
    scope: webhook.read
    summary: Consultar endpoint sem revelar segredos
    response_schema:
      $ref: "#/components/schemas/WebhookEndpointConfigResponse"
  get-authenticated-webhook-service:
    method: GET
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.read
    member_roles:
      - owner
      - admin
    summary: Consulta a permissão do parceiro e as integrações de entrega autenticada, sem consultar a Meta.
    optional_service: true
    idempotency_key_required: false
    meta_get: false
    query:
      - limit
      - offset
    response_description: Retorna ok, entitlement, integrations[], pagination={limit,offset,total} e limits. entitlement
      informa enabled, max_integrations, queue_max_events e queue_max_bytes. Consulte get-authenticated-webhook-billing
      para contratação, franquia, ciclo e excedente. Limite da página 1–100 (padrão 20), offset até 100000. limits
      informa os tetos técnicos de armazenamento, não a franquia comercial de consumo. integration contém id,
      partner_id, customer_id, instance_id, connection_generation, status, destination_url, signing_key_id,
      rotation_pending, previous_key_expires_at, queue_max_events, queue_max_bytes, enabled_at, drain_started_at,
      direct_restored_at, disabled_at, operation_error_code e usage_collected_at/usage_collection_started_at. queue
      contém pending, pending_bytes e checked_at quando houver consulta confirmada; ausência não significa zero. Não
      contém segredo, callback interno ou Meta Access Token. status pending é preparação; enabled é ativo; draining
      aguarda migração direta e/ou término da fila; disabled é encerrado; revoked é autorização encerrada. Confira
      direct_restored_at antes de afirmar que o modo direto foi restaurado.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  provision-authenticated-webhook:
    method: POST
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.write
    member_roles:
      - owner
      - admin
    summary: Valida o destino HTTPS e o GET do receptor, prepara uma integração pendente e apresenta o segredo uma única vez.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookProvisionAuthenticatedWebhook"
    response_description: Retorna integration pendente, credentials={key_id,signing_secret} e
      next_step=configure_receiver_then_activate. signing_secret é hexadecimal de 32 bytes e não será revelado em
      consultas. Configure o receptor com o key_id retornado. A preparação não inicia uma nova compra; confirme a
      contratação na consulta comercial. integration contém id, partner_id, customer_id, instance_id,
      connection_generation, status, destination_url, signing_key_id, rotation_pending, previous_key_expires_at,
      queue_max_events, queue_max_bytes, enabled_at, drain_started_at, direct_restored_at, disabled_at,
      operation_error_code e usage_collected_at/usage_collection_started_at. queue contém pending, pending_bytes e
      checked_at quando houver consulta confirmada; ausência não significa zero. Não contém segredo, callback interno ou
      Meta Access Token. status pending é preparação; enabled é ativo; draining aguarda migração direta e/ou término da
      fila; disabled é encerrado; revoked é autorização encerrada. Confira direct_restored_at antes de afirmar que o
      modo direto foi restaurado.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  activate-authenticated-webhook:
    method: POST
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.write
    member_roles:
      - owner
      - admin
    summary: Aplica o callback autenticado à instância e confirma o vínculo remoto antes de marcar o serviço ativo.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookActivateAuthenticatedWebhook"
    response_description: Retorna integration, propagated e propagation_max_seconds quando houver atualização de
      configuração. enabled só é gravado após confirmação da alteração. propagated=false exige acompanhar a propagação;
      não refazer o POST cegamente. integration contém id, partner_id, customer_id, instance_id, connection_generation,
      status, destination_url, signing_key_id, rotation_pending, previous_key_expires_at, queue_max_events,
      queue_max_bytes, enabled_at, drain_started_at, direct_restored_at, disabled_at, operation_error_code e
      usage_collected_at/usage_collection_started_at. queue contém pending, pending_bytes e checked_at quando houver
      consulta confirmada; ausência não significa zero. Não contém segredo, callback interno ou Meta Access Token.
      status pending é preparação; enabled é ativo; draining aguarda migração direta e/ou término da fila; disabled é
      encerrado; revoked é autorização encerrada. Confira direct_restored_at antes de afirmar que o modo direto foi
      restaurado.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  drain-authenticated-webhook:
    method: POST
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.write
    member_roles:
      - owner
      - admin
    summary: Solicita encerramento planejado do serviço opcional, restaurando o destino direto confirmado e concluindo as
      entregas anteriores.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookDrainAuthenticatedWebhook"
    response_description: Retorna integration em draining ou disabled e, quando confirmado, direct_mode_restored=true e
      meta_token_preserved=true. pending pode ser cancelado sem alterar callback. draining sem direct_restored_at ainda
      exige confirmação da restauração; disabled só encerra a fila conforme o estado reconciliado. Não desconecta o
      WhatsApp nem revoga token. integration contém id, partner_id, customer_id, instance_id, connection_generation,
      status, destination_url, signing_key_id, rotation_pending, previous_key_expires_at, queue_max_events,
      queue_max_bytes, enabled_at, drain_started_at, direct_restored_at, disabled_at, operation_error_code e
      usage_collected_at/usage_collection_started_at. queue contém pending, pending_bytes e checked_at quando houver
      consulta confirmada; ausência não significa zero. Não contém segredo, callback interno ou Meta Access Token.
      status pending é preparação; enabled é ativo; draining aguarda migração direta e/ou término da fila; disabled é
      encerrado; revoked é autorização encerrada. Confira direct_restored_at antes de afirmar que o modo direto foi
      restaurado.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  reconcile-authenticated-webhook:
    method: POST
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.write
    member_roles:
      - owner
      - admin
    summary: Confere o resultado de uma operação de callback ambígua e consulta o estado atual da integração e da fila.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookReconcileAuthenticatedWebhook"
    response_description: Retorna integration e queue={pending,pending_bytes}, quando disponíveis, além dos metadados de
      reconciliação. A consulta pode usar leitura remota controlada se houver callback incerto; não repete
      automaticamente o POST de configuração. Grave horário de consulta e não trate ausência de queue como zero.
      integration contém id, partner_id, customer_id, instance_id, connection_generation, status, destination_url,
      signing_key_id, rotation_pending, previous_key_expires_at, queue_max_events, queue_max_bytes, enabled_at,
      drain_started_at, direct_restored_at, disabled_at, operation_error_code e
      usage_collected_at/usage_collection_started_at. queue contém pending, pending_bytes e checked_at quando houver
      consulta confirmada; ausência não significa zero. Não contém segredo, callback interno ou Meta Access Token.
      status pending é preparação; enabled é ativo; draining aguarda migração direta e/ou término da fila; disabled é
      encerrado; revoked é autorização encerrada. Confira direct_restored_at antes de afirmar que o modo direto foi
      restaurado.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  rotate-authenticated-webhook-secret:
    method: POST
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.write
    member_roles:
      - owner
      - admin
    summary: Prepara uma nova chave de assinatura e só passa a usá-la depois da sua confirmação.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookRotateAuthenticatedWebhookSecret"
    response_description: prepare retorna integration, rotation_id, credentials={key_id,signing_secret} e
      next_step=install_new_key_then_confirm. confirm retorna integration, propagated e accept_previous_until. Mantenha
      a chave anterior até esse prazo; janela de transição de até 60 minutos. A preparação expira em 24 horas ou quando
      substituída. O verificador seleciona a chave pelo X-HookCloud-Key-Id. integration contém id, partner_id,
      customer_id, instance_id, connection_generation, status, destination_url, signing_key_id, rotation_pending,
      previous_key_expires_at, queue_max_events, queue_max_bytes, enabled_at, drain_started_at, direct_restored_at,
      disabled_at, operation_error_code e usage_collected_at/usage_collection_started_at. queue contém pending,
      pending_bytes e checked_at quando houver consulta confirmada; ausência não significa zero. Não contém segredo,
      callback interno ou Meta Access Token. status pending é preparação; enabled é ativo; draining aguarda migração
      direta e/ou término da fila; disabled é encerrado; revoked é autorização encerrada. Confira direct_restored_at
      antes de afirmar que o modo direto foi restaurado.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  get-authenticated-webhook-usage:
    method: GET
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.read
    member_roles:
      - owner
      - admin
    summary: Retorna agregados técnicos por dia UTC de admissão, preservando o cliente e a geração associados no momento da
      aceitação.
    optional_service: true
    idempotency_key_required: false
    meta_get: false
    query:
      - from
      - to
      - customer_id
      - instance_id
      - limit
      - offset
    response_description: Retorna rows, totals, pagination, from, to, unit=unique_webhook_envelope,
      period_basis=accepted_utc_day, timezone=UTC, collection=periodic, coverage=post_upgrade_admissions_only e
      last_collected_at. Apenas admissões após usage_collection_started_at de cada integração estão cobertas; tráfego
      anterior não foi reconstruído. Cada linha traz integration_id, day, partner_id, customer_id, instance_id,
      generation, revision, ingested_unique, payload_bytes, duplicates, delivery_attempts, retry_attempts,
      queue_rejected, delivered, expired, failed, connection_closed, configuration_changed, operator_canceled, pending,
      source_updated_at e collected_at. Resultados posteriores atualizam o dia de admissão; não criam nova unidade no
      mês da tentativa. totals agrega o conjunto filtrado; rows é uma página. Envelope não é mensagem; bytes e retries
      são dimensões separadas. Ausência de coleta não comprova zero. Não contar retries como novas admissões. Para
      franquia, unidades de 64 KiB e excedente financeiro, use get-authenticated-webhook-billing; o ciclo comercial não
      coincide necessariamente com dias UTC completos.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  get-authenticated-webhook-billing:
    method: GET
    auth: partner_api_key_or_member_jwt
    scopes:
      - webhook.read
    member_roles:
      - owner
      - admin
    summary: Consulta a contratação de Webhook Autenticado + Fila, o ciclo de consumo e o excedente estimado.
    optional_service: true
    idempotency_key_required: false
    meta_get: false
    query: []
    response_description: "Retorna
      billing={status,enabled,checkout_available,base_amount_cents,currency,included_units,unit_bytes,excess_per_1000_c\
      ents,period,cancel_at_period_end,breakdown,breakdown_limit,usage_updated_at,summary_partial,renewal_processing,re\
      newal_processing_grace_until,billing_attention}. status: not_subscribed, pending_payment, active, past_due,
      canceled ou internal (piloto sem cobrança). period contém
      id,start,end,used_units,remaining_units,excess_units,estimated_excess_cents,percent_used; pode não existir sem
      ciclo confirmado. breakdown contém customer_id,instance_id,units, limitado a 100 grupos; o agregado period
      considera todos. renewal_processing indica conciliação de renovação de ciclo antes pago, por no máximo 24 horas
      até renewal_processing_grace_until; não se aplica à compra inicial nem ao cancelamento. billing_attention contém
      requires_operator_review, export_count, invoice_count e codes (relay_billing_export_uncertain ou
      relay_billing_invoice_requires_review): conferência interna do fechamento, sem alterar enabled e sem indicar
      pendência do cliente ou da Meta; não gere nova contratação por esse aviso. Mensalidade 9990 centavos BRL; franquia
      de 100000 unidades; bloco de 65536 bytes; excedente de 90 centavos/mil proporcional. Ausência de medição não é
      zero. A medição é periódica e a estimativa pode mudar após coleta final."
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  create-authenticated-webhook-checkout:
    method: POST
    auth: member_jwt
    scopes: []
    member_roles:
      - owner
    summary: Cria o checkout seguro da Stripe para a contratação mensal do adicional pelo owner.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookCreateAuthenticatedWebhookCheckout"
    response_description: Retorna checkout_url para o checkout seguro da Stripe. O servidor fixa o retorno ao Portal; não
      envie URLs de retorno. Redirecionar ou voltar do checkout não confirma contratação. Consulte
      get-authenticated-webhook-billing; a liberação exige pagamento confirmado. R$ 99,90/mês, 100 mil unidades, sem
      trial; excedente de R$ 0,90/mil proporcional após o ciclo. Não cria mensalidade por número.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
  cancel-authenticated-webhook-subscription:
    method: POST
    auth: member_jwt
    scopes: []
    member_roles:
      - owner
    summary: Altera apenas a renovação de Webhook Autenticado + Fila, preservando pacotes de números.
    optional_service: true
    idempotency_key_required: true
    meta_get: false
    request_schema: "#/components/schemas/AuthenticatedWebhookCancelAuthenticatedWebhookSubscription"
    response_description: Retorna estado da alteração. Consulte get-authenticated-webhook-billing para confirmação do ciclo
      e cancel_at_period_end. O consumo excedente continua devido. Esta ação não desconecta o WhatsApp nem revoga token;
      a mudança de transporte exige ação explícita e drenagem segura por integração. Falha de cobrança não autoriza
      envio sem assinatura.
    reference: https://developers.hookcloud.app/authenticated-webhooks.html
externalDocs:
  description: Documentação completa
  url: https://developers.hookcloud.app/api-reference.html
x-meta-graph:
  server: https://graph.facebook.com/v26.0
  authentication: Bearer Meta Access Token; não usar a Partner API Key
  docs: https://developers.hookcloud.app/messaging.html
  recipient:
    field: recipient
    description: BSUID completo ou Parent BSUID habilitado; obter de webhook Meta, nunca gerar
    phone_compatibility: to
    precedence: Se to e recipient coexistem, a Meta prioriza to
    forbidden_assumption: user_id e username não substituem recipient no body de envio
  routes:
    messages:
      method: POST
      path: /{phone_number_id}/messages
      examples:
        text_bsuid:
          messaging_product: whatsapp
          recipient: BR.BSUID_RECEBIDO_DA_META
          type: text
          text:
            body: Olá, como podemos ajudar?
        mark_read:
          messaging_product: whatsapp
          status: read
          message_id: wamid.DA_MENSAGEM_RECEBIDA
    templates:
      list: GET /{waba_id}/message_templates
      create: POST /{waba_id}/message_templates
      get: GET /{template_id}
      update: POST /{template_id}
      delete: DELETE /{waba_id}/message_templates?name={name}&hsm_id={template_id}
    media:
      upload: POST /{phone_number_id}/media
      get: GET /{media_id}
      delete: DELETE /{media_id}
    username:
      get: GET /{phone_number_id}/username
      reserve_or_update: POST /{phone_number_id}/username
      suggestions: GET /{phone_number_id}/username_suggestions
      delete: DELETE /{phone_number_id}/username
      default_transfer_action: none

x-partner-health-contract:
  schema_version: partner-health-v2
  docs: https://developers.hookcloud.app/connection-health.html
  health_schema:
    $ref: "#/components/schemas/PartnerConnectionHealth"
  capacity_schema:
    $ref: "#/components/schemas/PartnerCapacityEvidence"
  legacy_fields_preserved: [ status, connection_health.state, ready_to_operate, number_limit, used_numbers, available_numbers ]
  applies_to:
    [
      get-partner-instance,
      list-partner-instances,
      refresh-partner-instance-meta,
      get-connect-session,
      complete-meta-onboarding
    ]
  payment_only: Complete exact WABA with exclusively141006 keeps inventory active without manufacturing messaging available or GREEN.
  local_reads: Lists do not query Graph per row. Explicit refresh is authorized and bounded.
  registration_pending:
    phone_statuses: [ PENDING, PENDING_REGISTRATION, UNVERIFIED ]
    messaging_reason: meta_registration_not_confirmed
    messaging_state: unknown
    health_state: unknown
    ready_to_operate: false
    inventory_state: attention
    issue_code: meta_phone_not_confirmed_connected
    technical_status_preserved: true
    adds_meta_get: false
    note: Exact current phone evidence takes precedence over WABA AVAILABLE; genuine blocks, limitations or restrictions
      retain priority. Does not create BAN, disconnect, reset identity or change billing/capacity.

x-partner-health-history:
  route: get-partner-instance-history
  items: [ id, generation, operation_id, health, created_at ]
  response_fields:
    [
      schema_version,
      instance_id,
      connection_generation,
      items,
      total,
      page,
      page_size,
      history_complete,
      coverage
    ]
  cached_gets_only: true
  observation_ttl_seconds: 1200
  refresh_cooldown_seconds: 30
  refresh_errors:
    [
      health_refresh_rate_limited,
      stale_connection_generation,
      instance_operation_in_progress,
      operation_lease_lost,
      health_evidence_changed
    ]
  webhook_event: hookcloud.instance.health_updated
  webhook_fields:
    [
      event_id,
      schema_version,
      event,
      partner_id,
      instance_id,
      customer_id,
      connection_generation,
      connection_health,
      observed_at
    ]

x-partner-request-correlation:
  header: X-Request-ID
  json_fields: [ request_id, error.request_id ]
  note: Request IDs are trace identifiers, not credentials or idempotency keys.
x-partner-reservation-expiry:
  new_safe_reservation_ttl_seconds: 900
  reconciliation_trigger: next_reservation_request
  get_releases_expired_reservations: false
  never_release_by_age: [ legacy, number_in_use, active_or_uncertain_callback, partial_setup, processing_or_needs_review_remote_operation ]

x-partner-integration-policy:
  documentation: https://developers.hookcloud.app/security.html#integration-policy
  full_endpoint_reference: https://developers.hookcloud.app/endpoint-catalog.html
  meta_get_cache_required: true
  rules:
    [
      server_side_cache,
      webhook_first,
      pagination,
      bounded_concurrency,
      retry_after_when_present,
      bounded_exponential_backoff,
      no_permission_error_loop
    ]
  local_gets: Read stored HookCloud data and observation timestamps; do not call Meta for each row.
  enforcement_notice: Excesso recorrente pode gerar revisão da integração e solicitação de correções pela HookCloud; não
    significa suspensão automática.
x-partner-managed-identity:
  new_instance_key: Generated by server; custom new key returns instance_key_server_managed.
  existing_instance_key: Accepted only to reuse its existing instance for compatibility.
  initial_instance_name: Optional provisional name; approved Meta verified_name becomes canonical automatically.
  rename_error: instance_name_managed_by_meta
  fields: [ meta_business_id, meta_business_name, meta_waba_id, meta_waba_name, meta_synced_at, meta_sync ]
  creation_idempotency:
    operations: [ create-customer-instance, create-partner-instance ]
    key: Idempotency-Key header preferred over idempotency_key body; 8..200 characters.
    scope: Partner and endpoint operation; retry the same endpoint and identical business payload.
    replay: Original HTTP status and body, including reused, Connect URL and expiry; no duplicate session and no expiry
      extension.
    fingerprint: Object-key order and root transport selectors do not change the fingerprint; nested metadata remains
      business input.
    changed_payload: HTTP 409 idempotency_key_reused.
    in_progress: HTTP 409 operation_in_progress; retry the same request after a bounded delay.
    interrupted: HTTP 409 idempotency_result_unavailable; inspect the instance before a new operation.
    response_storage_failure: HTTP 503 idempotency_result_pending; creation may already be committed, inspect before retrying with a new key.
    current_state: Use get-partner-instance; the replay is the original creation snapshot.
    renew_link: Use create-connect-session for the existing instance; creation retry does not renew a link.
    new_key: A distinct operation, never an automatic timeout retry strategy.
    availability: Applies to creation requests first processed after this contract deployment; historical responses cannot
      be reconstructed.
  changed_event: hookcloud.instance.updated
  sync: Every-minute bounded batch; target 5 minutes pending-name or 15 minutes healthy with backoff and queue. Cached GET
    does not make a live Meta request.
x-partner-customer-risk:
  schema_version: partner-customer-risk-v2
  scope: customer
  window_days: 30
  limits: { ban: 20, red: 5 }
  blocking: New connections blocked at either threshold; package capacity does not override. Existing numbers remain connected.
  deduplication: Repeated observations of the same confirmed episode count once; restoration does not erase its occurrence
    from the window.
  event: hookcloud.customer.risk_updated
  event_data: [ partner_customer_id, customer_risk ]
  fields:
    [
      ban_count_30d,
      red_count_30d,
      block_new_connections,
      reasons,
      next_exit_at,
      next_unlock_at,
      history_complete,
      history_started_at
    ]

x-partner-customer-avatar:
  fields: [ avatar_url, avatar_url_expires_at, avatar_updated_at ]
  instance_list_field: customer_avatar_url
  storage: private
  max_side_pixels: 4096
  max_total_pixels: 16000000
  animated_images: false
  signed_url_ttl_seconds: 300
  meta_profile_changed: false
  refresh: Requery the local customer endpoint when the signed URL expires; never poll Meta for this photo.

x-partner-meta-optional-profile:
  schema:
    $ref: '#/components/schemas/PartnerMetaOptionalProfile'
  instance_projection:
    $ref: '#/components/schemas/PartnerMetaProfileCompatibilityFields'
  applies_to: [ get-partner-instance, list-partner-instances, refresh-partner-instance-meta ]
  health_path: connection_health.meta_profile
  cache_ttl_seconds: 3600
  health_observation_ttl_seconds: 1200
  event: hookcloud.instance.health_updated
  material_changes_only: true
  timestamps_alone_emit_event: false
  current_limit_graph_field: whatsapp_business_manager_messaging_limit
  current_limit_scope: business_portfolio
  current_limit_window_hours: 24
  current_limit_window_kind: rolling
  remaining_capacity_returned: false
  optional_failure_changes_operational_health: false
  normal_health_refresh_may_reuse_profile_cache: true
  note: 'Use checked_at de cada fato. Eventos relevantes podem antecipar a leitura respeitando fila/backoff. Username
    reserved ou lifecycle_status null não confirma ativo; dados antigos ficam neutros. Limite Meta não é saldo nem
    pacote HookCloud.'
  official_limit_reference: https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits

x-partner-first-connection:
  field: first_connected_at
  type: ISO 8601 UTC timestamp or null
  read_only: true
  applies_to: [ list-partner-instances, get-partner-instance, refresh-partner-instance-meta ]
  identity_scope: partner_id and meta_phone_number_id
  meaning: Earliest retained evidence of a confirmed technical connection for this number identity within the partner, not
    the complete lifetime history.
  preserved_on: [ reconnect_same_number, move_between_customers_in_same_partner, merge_customers_in_same_partner ]
  identity_change: A different Meta phone identity or another partner never inherits the previous identity history.
  missing_evidence: Return null and display Não informado; never replace with created_at, customer creation date or connected_at.
  not_a_health_confirmation: Does not confirm current messaging availability; check current health evidence and its observation timestamps.

x-partner-identity-replacement:
  normal_reconnect: 'Use mode=reconnect. Após nova autorização e prova Graph do mesmo número, WABA e portfólio, IDs novos
    são atualizados automaticamente, com o vínculo anterior encerrado. Não envie IDs manuais.'
  automatic_reason: automatic_same_phone_identity_replacement
  automatic_errors: [ replacement_phone_mismatch, replacement_phone_unconfirmed, replacement_business_unconfirmed ]
  route: create-connect-session
  mode: replace
  required: [ expected_connection_generation, replacement_target.meta_waba_id, replacement_target.meta_phone_number_id ]
  optional: [ replacement_target.meta_business_id ]
  prerequisites: 'Vínculo anterior canceled/inativo, occupies_slot false, slot_state released; confirmação de
    callback+deregistro ou perda de vínculo por webhook Meta assinado. Cobrança, risco, capacidade e conflito global de
    identidade continuam verificados.'
  authorization_scope: [ partner_id, customer_id, instance_id, connect_session_id, previous_generation, target_identity ]
  stable_fields: [ instance_id, instance_key ]
  automatic_replacement: false
  same_display_phone_is_proof: false
  history_committed_after: confirmed_new_connection_snapshot
  public_history: 'connection_history readonly, até 50 itens; histórico completo preservado na auditoria. Não conta como
    outra instância ativa nem como capacidade ocupada.'
  portal_group: 'Substituídos, com subtipos no card; sem novas abas para Transferidos/Encerrados.'
  event: instance.identity_replaced
  event_schema:
    $ref: '#/components/schemas/PartnerIdentityReplacedEvent'
  instance_fields_schema:
    $ref: '#/components/schemas/PartnerInstanceLifecycleFields'
  errors:
    [
      invalid_replacement_request,
      replacement_requires_confirmed_disconnect,
      stale_connection_generation,
      replacement_authorization_stale,
      replacement_identity_unchanged,
      replacement_target_mismatch,
      instance_operation_in_progress
    ]
  uncertain_write: 'Consulte o resultado antes de repetir; não gere outra chave/cliente para contornar uma limpeza incerta.'
x-partner-meta-link-removal:
  public_field: meta_link
  states: [ removed, access_unconfirmed, unknown ]
  confirmed_source: signed_meta_webhook
  reasons: [ partner_removed, app_uninstalled, waba_deleted ]
  graph_errors_are_removal_proof: false
  event: instance.meta_link_removed
  event_schema:
    $ref: '#/components/schemas/PartnerMetaLinkRemovedEvent'
  evidence_field: data.meta_link_state
  evidence_source_field: data.meta_link_state.evidence_source
  local_authorization: disabled
  capacity_release_reason: signed_meta_link_removal
  remote_cleanup_confirmed: false
  remote_callback_status: unavailable
  remote_authorization: unknown
  note: 'Somente evidência assinada, com app/escopo e geração atuais, confirma a perda de vínculo. Erros Graph 100/33/190
    não geram BAN, desconexão ou liberação de vaga por inferência. Este evento difere de instance.canceled por limpeza
    remota concluída.'

x-partner-registration-onboarding:
  status_route: get-partner-onboarding-status
  sequence: [ stripe_card_and_trial, mandatory_owner_registration, verified_global_webhook, operations ]
  input_channel: owner_authenticated_partner_portal
  accepts_partner_api_key_submission: false
  person_types:
    pf: [ full_name, mobile, cpf ]
    pj: [ legal_name, cnpj, full_name_of_responsible, mobile ]
  validation: format_and_check_digits
  registry_verified: false
  identity_verified: false
  mobile_ownership_verified: false
  review: manual_outside_automatic_activation
  error: { http_status: 409, code: partner_registration_required }
  note: 'Mesmo um CPF/CNPJ matematicamente válido pode ser inexistente ou pertencer a terceiro. Não há consulta externa.
    Os endpoints de clientes do parceiro não substituem o cadastro do contratante. Enquanto houver pendência, mantenha
    as operações bloqueadas; consulta de status e rotas de recuperação/cancelamento de cobrança continuam acessíveis
    conforme suas permissões.'

x-partner-webhook-onboarding:
  status_route: get-partner-onboarding-status
  protected_operations: [ upsert-partner-customer, create-customer-instance, create-partner-instance, create-connect-session ]
  customer_upsert_note: A exigência vale para cadastro novo; atualização cadastral existente não cria outro cliente.
  prerequisite: Endpoint primário habilitado com evidência GET salva para a URL atual. Validar sem salvar não libera.
  verification: { method: GET, mode: subscribe, challenge: "1234", expected_http_status: 200, expected_body: "1234" }
  error: { http_status: 409, code: partner_webhook_verification_required }
  post_note: Mantenha POST ativo na mesma URL. GET válido não comprova recebimento, assinatura ou persistência de POST.
x-partner-authenticated-webhooks:
  default_mode: direct
  activation: paid_subscription_or_explicit_internal_grant_and_receiver_ready_per_integration
  billing_enabled: true
  commercial:
    product: Webhook Autenticado + Fila
    currency: brl
    base_amount_cents: 9990
    included_units: 100000
    unit_bytes: 65536
    excess_per_1000_cents: 90
    prorated_excess: true
    trial: false
    carry_over: false
    scope: partner_account
    payment_before_activation: true
    pilot: explicit_internal_grant_without_charge
    cycle: exact subscription period, not calendar UTC days
    owner_jwt_required_for_purchase: true
  retention_seconds: 86400
  max_attempts: 32
  body_limit_bytes: 2097152
  capacity: Read effective queue_max_events and queue_max_bytes per integration; time retention is not unlimited capacity.
  signature:
    algorithm: HMAC-SHA256
    key_encoding: 32-byte secret encoded as 64 hexadecimal characters
    prefix: v1\n{delivery_id}\n{timestamp}\n{integration_id}\n
    body: original bytes, never JSON.stringify
    header: X-HookCloud-Signature
    format: v1=hex
    key_selector: X-HookCloud-Key-Id
    timestamp_tolerance_seconds: 300
  rotation:
    phases:
      - prepare
      - confirm
    preparation_seconds: 86400
    previous_key_overlap_seconds: 3600
    overlap_blocks_further_rotation: true
  delivery: Persist and deduplicate durably before HTTP 2xx. At-least-once; no global ordering or message history.
    Distinct from application event signature and its five attempts.
  cancellation: Use drain-authenticated-webhook; preserve receiver until queue and propagation are confirmed.
    Configuration propagation may take 15 seconds; an in-flight POST can still arrive. Minimum 30-second grace after
    restoring direct delivery before disabling. Meta token remains available; WhatsApp stays connected.
  retention_scope: 24-hour retention is operational application retention, not physical erasure of infrastructure recovery
    copies. Encrypted prior versions may remain in Cloudflare SQLite PITR for up to 30 days; no subscriber-searchable
    message history.
  infrastructure_recovery_reference: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
  consumption:
    unit: unique_authenticated_admitted_envelope_per_started_65536_bytes
    formula: sum(ceil(original_envelope_bytes/65536)) at unique admission
    duplicates_and_retries: not recounted
    invalid_or_rejected_before_admission: not counted
    admitted_but_not_delivered: counted
    billing_query: get-authenticated-webhook-billing
    technical_report_period_basis: accepted_utc_day
    coverage: post_upgrade_admissions_only
    timezone: UTC
    dates: technical report from and to inclusive, at most 31 days
    collection: periodic, inspect usage_updated_at and summary_partial; not instant
