Pular para conteúdo

Referência de payloads

Convenções

Exemplos abaixo usam nomes sintéticos, example.invalid e placeholders. São contratos para leitura, não ordens de envio real. Campos como <TELEFONE_FICTICIO> e <CPF_FICTICIO> precisam ser substituídos por fixtures locais apropriadas para passar validadores; placeholders deliberadamente não são documentos/telefones válidos.

Fronteira Fonte principal
WhatsApp inbound src/core/integrations/whatsapp/adapter.py
WhatsApp outbound src/core/integrations/whatsapp/cloud.py
Saída IA src/core/application/parser.py, action_mapping.py
Sessão/mensagem/demanda src/core/domain/ e src/core/integrations/dynamodb/serialization.py
CRM src/core/integrations/hubspot/payloads.py
eGestor src/core/integrations/egestor/payloads.py
Handoff src/core/integrations/handoff/interface.py, email.py, whatsapp.py
Fila / sync src/webhook_handler/app.py, src/core/integrations/dynamodb/crm_sync.py

Inbound Meta e normalização

Envelope relevante para POST /webhook:

{
  "object":"whatsapp_business_account",
  "entry":[{
    "id":"<WABA_ID>",
    "changes":[{
      "field":"messages",
      "value":{
        "messaging_product":"whatsapp",
        "metadata":{"phone_number_id":"<PHONE_NUMBER_ID>"},
        "messages":[{
          "from":"<TELEFONE_SOMENTE_DIGITOS>",
          "id":"wamid.EXEMPLO_001",
          "timestamp":"1789387200",
          "type":"text",
          "text":{"body":"Quero informações sobre uma análise de amostra sintética."}
        }]
      }
    }]
  }]
}

extract_inbound_messages() percorre todas as entradas e mudanças, não apenas a primeira mensagem. _normalize_sender() retira não dígitos e prefixa +; sem dígitos lança ValueError. O ID original é preservado. Timestamp ausente/inválido usa UTC atual. Mídia produz caption quando existe ou [tipo]; tipo desconhecido é tratado como document. Nenhum binário é baixado por esse adapter.

O contrato interno produzido por WhatsAppCloudInboundMessage.to_internal_payload():

{
  "phone_number":"<TELEFONE_FICTICIO_NORMALIZADO>",
  "content":"Texto fictício",
  "message_type":"text",
  "instance":"gogenetic",
  "provider_message_id":"wamid.EXEMPLO_001",
  "received_at":"2026-09-14T12:00:00+00:00",
  "phone_number_id":"<PHONE_NUMBER_ID>"
}

O adapter Cloud atual constrói a instância default gogenetic; não existe nesse adapter um mapeamento automático de phone_number_id para goyou. O payload interno/emulador pode carregar instance explicitamente. provider_message_id ausente na rota interna ganha UUID no webhook. Callbacks contendo só status não produzem entradas de cliente.

Outbound WhatsApp

WhatsAppClient.send_message(to=..., text=...) monta POST /{version}/{phone_number_id}/messages, com Authorization: Bearer <TOKEN> e JSON:

{
  "messaging_product":"whatsapp",
  "recipient_type":"individual",
  "to":"<TELEFONE_SOMENTE_DIGITOS>",
  "type":"text",
  "text":{"preview_url":false,"body":"Mensagem fictícia de atendimento."}
}

O adapter extrai messages[0].id da resposta quando presente e retorna StubResponse. Falhas HTTP ou de transporte seguem a política em WhatsApp. O nome mock_id é compartilhado entre stub e clientes reais e pode conter ID real do provider.

Saída de agente e resultado LLM

{"s":1,"qualification_data":{"tipo_amostra":"amostra sintética"}}Quantas amostras você pretende analisar?

ParsedLLMResponse guarda o inteiro s, o objeto completo em json_part e o texto em message. O formato não é JSON puro: não aplique json.loads() à string inteira. Veja parser e reparo.

LLMCallResult tem raw_response: str, model: str, tokens_input: int, tokens_output: int, latency_ms: int. OpenAIClient transforma messages em input de responses.create, opcionalmente inclui reasoning.effort, lê output_text e usage. Não usa response_format/JSON Schema no request atual; o contrato é instruído e validado pela aplicação.

Session: campos persistidos

Session é dataclass mutável com identidade protegida. O serializer grava strings de enum e timestamps ISO; campos novos precisam manter compatibilidade com registros antigos. A chave DynamoDB/GSI é adicionada pelo repository, fora dos campos abaixo.

Campos Tipo / default Significado
session_id, phone_number string, obrigatórios Identidade imutável
whatsapp_instance gogenetic/goyou, obrigatório Marca/instância imutável
created_at datetime UTC, obrigatório Criação imutável
state SessionState, NEW Estado de domínio
current_agent AgentName ou null Agente atribuído; não implica que está executando
segment, intent Enums ou null Classificação
service_identified string ou null Nome canônico
qualification_data, cadastral_data objetos, {} Campos por merge
collected_packages lista de strings, [] IDs já injetados
context_enrichment_count inteiro, 0 Contador resetado por inbound do bot
updated_at, closed_at datetime; segundo opcional Última mudança e encerramento
handoff_reason HandoffReason ou null Motivo do encaminhamento
assigned_to telefone ou null Operador que assumiu
handoff_renotified_at datetime ou null Última renotificação
handoff_renotification_count inteiro, 0 Quantidade de renotificações
handoff_customer_renotified_at datetime ou null Aviso correspondente ao cliente; serializer omite chave quando null
egestor_synced, hubspot_synced booleanos, false Flags históricas; CRM lido é enriquecido com projeção
hubspot_contact_id, hubspot_deal_id string ou null Identificadores externos
crm_terminal_stage string ou null Stage lógico terminal opcional
egestor_payload, hubspot_payload objetos ou null Payloads preparados/guardados
operational_summary objeto, string legada ou null Resumo humano estruturado
previous_session_id string ou null, imutável Vínculo entre sessões
recognized_name string ou null Nome reconhecido de sessão anterior
pending_subject_change_text string ou null Solicitação mantida enquanto pede confirmação
retry_count inteiro, 0 Falhas de validação cadastral

Exemplo abreviado de snapshot de domínio:

{
  "session_id":"sessao-exemplo",
  "phone_number":"<TELEFONE_FICTICIO>",
  "whatsapp_instance":"gogenetic",
  "state":"DATA_COLLECT",
  "current_agent":"coleta",
  "segment":"PESQUISA",
  "intent":"servico",
  "service_identified":"Metagenoma_Shotgun",
  "qualification_data":{"tipo_amostra":"amostra sintética"},
  "cadastral_data":{"nome":"Pessoa Exemplo","email":"teste@example.invalid"},
  "collected_packages":["SAMPLE_INSTRUCTIONS"],
  "context_enrichment_count":1,
  "created_at":"2026-09-14T12:00:00+00:00",
  "updated_at":"2026-09-14T12:01:00+00:00",
  "previous_session_id":null
}

Message Log e demanda

Message exige message_id, session_id, phone_number, direction, message_type, content, timestamp, state_before, state_after. Direções: INBOUND/OUTBOUND/SYSTEM. Tipos: text/image/audio/video/document/sticker/system.

Campos opcionais: debounced_with, llm_raw_response, llm_parsed_json, clean_message_sent, agent, operator_phone, provider_message_id, context_packages_active, llm_model, tokens_input, tokens_output, latency_ms, error. error="superseded" torna um outbound inadequado para replay/histórico IA. Campos de log de conteúdo são dados sensíveis de operação; a sanitização do logger não remove automaticamente conteúdo persistido no Message Log.

OutOfPortfolioDemand contém demand_id, session_id, phone_number, push_name, service_requested, estimated_segment, whatsapp_instance, original_message, created_at. from_triagem() cria UUID e horário. O serviço solicitado é texto livre e não precisa constar no catálogo oferecido.

CRM: contato e negócio

build_contact_payload(cadastral_data, segment, phone_number=None) gera:

{
  "properties":{
    "firstname":"Pessoa",
    "lastname":"Exemplo",
    "email":"teste@example.invalid",
    "phone":"<TELEFONE_FICTICIO>",
    "address":"Rua Exemplo, número fictício",
    "city":"Cidade Exemplo",
    "state":"PR",
    "zip":"<CEP_FICTICIO>",
    "segmento":"PESQUISA",
    "origem_lead":"Agente Digital WhatsApp",
    "cpf_cnpj":"<CPF_FICTICIO>"
  }
}

Sem nome, firstname="Lead WhatsApp"; sem telefone cadastral, usa fallback do argumento. Propriedades podem ser null no builder; o cliente HTTP prepara a forma final conforme operação. A origem legada hubspot é compatibilidade de contrato; o host configurado pode ser o CRM GoGenetic.

build_deal_payload() produz propriedades e associação opcional:

{
  "properties":{
    "dealname":"Metagenoma_Shotgun — Pessoa Exemplo",
    "pipeline":null,
    "dealstage":null,
    "hubspot_owner_id":null,
    "segmento":"PESQUISA",
    "origem_lead":"Agente Digital WhatsApp",
    "servico_solicitado":"Metagenoma_Shotgun",
    "descricao_solicitacao":"Solicitação fictícia de análise.",
    "historico_conversa":"Resumo fictício para o operador.",
    "qualification_data_json":"tipo_amostra=amostra sintética",
    "telefone":"<TELEFONE_FICTICIO>",
    "email":"teste@example.invalid",
    "cpf_cnpj":"<CPF_FICTICIO>"
  },
  "associations":[{
    "to":{"id":"contato-exemplo"},
    "types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":3}]
  }]
}

qualification_data_json é nome histórico: seu conteúdo é string chave=valor; ..., não JSON serializado. pipeline/owner são preenchidos pela configuração do cliente; stage depende da operação/projeção. Sem contact_mock_id, associations começa vazia. O worker é o escritor dos deals; consulte CRM para request HTTP final, lookup e idempotência.

eGestor: cliente

build_customer_payload(cadastral_data=...) gera o corpo atual de /contatos:

{
  "nome":"Pessoa Exemplo",
  "tipo":["cliente"],
  "cpfcnpj":"<DOCUMENTO_FICTICIO_SOMENTE_DIGITOS>",
  "emails":["teste@example.invalid"],
  "fones":["<TELEFONE_FICTICIO>"]
}

cpfcnpj aceita origem cpfcnpj, cpfCnpj ou cpf_cnpj, nessa precedência, e remove não dígitos. Se nenhum dígito existir, a propriedade é omitida. nome é acessado como obrigatório; ausência pode gerar KeyError. emails/fones só são incluídos quando há valor. Endereço/cidade/UF/CEP coletados não são enviados por este builder atual. Não confunda o corpo implementado com uma especificação futura de API própria compatível.

Handoff e resumo operacional

HandoffNotifier.notify(session) recebe a própria Session, não um payload HTTP genérico. O notifier SES deriva Source/Destination/Message; o notifier WhatsApp monta texto com contexto e encaminha aos operadores disponíveis. operational_summary tem contrato próprio:

{
  "motivo_encerramento":"pedido_cliente",
  "servico_identificado":null,
  "segmento":"PESQUISA",
  "pontos_chave":["Cliente fictício solicitou contato humano"],
  "dados_coletados":{},
  "proxima_acao":"Ler o histórico e continuar o atendimento",
  "resumo_texto":"A pessoa pediu atendimento humano para esclarecer a solicitação."
}

summary_text() aceita esse objeto, uma string legada ou null. O gerador faz normalização e fallback; as sete chaves estão presentes no objeto normalizado. Não usa o contrato de action codes.

StubResponse é comum aos adapters:

{"success":false,"mock_id":null,"detail":"falha_ficticia","error_kind":"transient"}

error_kind aceita transient, permanent, uncertain ou null. HandoffNotifyResult acrescenta notified, operator_phone, queue_position. Um retorno success=true, notified=false, queue_position=1 significa que o encaminhamento ficou na fila, não que o operador recebeu mensagem.

Receipts, SQS e projeção CRM

InboundTurn contém telefone, ID, conteúdo, recebido e instância. A mensagem SQS é apenas:

{"phone_number":"<TELEFONE_FICTICIO>","message_id":"wamid.EXEMPLO_001"}

Receipt DynamoDB adiciona ttl, agent_routed_at, dispatched_at, sync_claimed_at e completed_at conforme progresso. O marcador latest lista pending_message_ids. A Lambda retorna batchItemFailures com IDs SQS, não IDs WhatsApp.

Projeção CRM_SYNC#{session_id}/META guarda desired (stage, terminal, contact, deal), revision SHA-256, confirmed_revision, etag, contact_id, deal_id, creation_started, status, attempts, next_attempt_at, pending_since, timestamps e campos de lease/erro quando aplicáveis. Estados da projeção: PENDING/SYNCED/BLOCKED/UNCERTAIN. Esses estados não pertencem a SessionState.

O job src/crm_sync/app.py é agendado e ignora conteúdo de negócio do evento para buscar pendências na tabela. Retorna {"status":"disabled"} quando desabilitado ou {"status":"ok","metrics":{...}} com Pending, Blocked, Uncertain, OldestPendingSeconds. A intenção é durável no DynamoDB; não há uma segunda fila SQS de CRM no caminho atual.

Testes e evolução dos contratos

Fontes de verificação: tests/unit/integrations/test_whatsapp_adapter.py, test_whatsapp_cloud_client.py, test_dynamodb_repos.py, test_egestor_http_client.py, test_hubspot_http_client.py, tests/unit/application/test_parser.py, test_operational_summary.py, test_crm_sync.py e tests/unit/lambdas/test_crm_sync.py.

Quando mudar um payload, identifique quem constrói, quem serializa e quem lê registros anteriores. Adicione teste de round-trip para persistência, request capturado para HTTP e replay para saída de agente. Não substitua propriedades legadas por nomes novos sem atualizar todos os consumidores. Veja erros e arquitetura.