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-partner-customer-history
              - get-partner-instance
              - 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-webhook-endpoint
        - 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: 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.
      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/TemplateRefreshRequest"
                - $ref: "#/components/schemas/DeliveryStatusRequest"
                - type: object
                  required:
                    - route
                  properties:
                    route:
                      type: string
      responses:
        "200":
          description: Sucesso
        "201":
          description: Criado
        "202":
          description: Aceito para processamento
        "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"
                - 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
        "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
        "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:
    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string
            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
        - instance_key
      properties:
        route:
          const: create-customer-instance
        partner_customer_id:
          type: string
          format: uuid
        external_customer_id:
          type: string
        instance_key:
          type: string
          description: Imutável e único no partner.
        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
        metadata:
          type: object
    CreateConnectSessionRequest:
      type: object
      required:
        - route
      properties:
        route:
          const: create-connect-session
        instance_id:
          type: string
          format: uuid
        instance_key:
          type: string
        mode:
          type: string
          enum:
            - connect
            - reconnect
          default: connect
        return_url:
          type:
            - string
            - "null"
          format: uri
        cancel_url:
          type:
            - string
            - "null"
          format: uri
        ttl_hours:
          type:
            - number
            - "null"
    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"
    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:
              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."
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
  get-partner-instance:
    method: GET
    auth: partner_api_key
    summary: Consultar instância
  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: Inativar instância
    methods:
      - POST
      - PATCH
      - DELETE
  deactivate-partner-customer:
    method: POST
    auth: partner_api_key
    summary: Inativar cliente
    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
  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
  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)
  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-webhook-endpoint:
    method: GET
    auth: partner_api_key
    scope: webhook.read
    summary: Consultar endpoint sem revelar segredos
    response_schema:
      $ref: "#/components/schemas/WebhookEndpointConfigResponse"
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
