# Receptor AtendroZAP no Atendro Contrato para criar um **novo receptor específico do AtendroZAP**. O endpoint do provedor anterior não é reutilizado. A implementação e a URL definitiva no Atendro serão feitas separadamente; nenhum destino real está predefinido aqui. ## Cadastro global por servidor No console [wpp.atendro.cloud](https://wpp.atendro.cloud): **Servidores → servidor → Configurações de webhook**. Informe URL HTTPS pública, segredo de assinatura e eventos. Salve com entrega habilitada quando o receptor estiver pronto. Credenciais reais ficam no backend privado. Via API, `GET /v1/webhook` consulta e `POST /v1/webhook` salva na URL do servidor selecionado, ambos com header `admintoken`. A documentação fica na central; o webhook global continua sendo uma configuração de cada servidor. O corpo de cadastro é: ```json { "url": "https://seu-atendro.example/webhooks/atendrozap", "secret": "", "events": ["connection", "messages", "messages_update", "messages_edit", "groups", "limits", "undecryptable"], "enabled": true, "excludeMessages": [] } ``` O segredo deve ter até 256 caracteres ASCII sem espaços. Para gerar um novo, `openssl rand -hex 32` produz 32 bytes aleatórios; guarde o resultado somente no servidor e no receptor. É diferente do `admintoken` e dos tokens de instância. Com `ATENDROZAP_ENVELOPE_KEY`, fica cifrado no banco; as consultas só devolvem `hasSecret` e `secretSealed`, nunca o valor. | Ação | Resultado | |---|---| | Salvar o global | Aplica às instâncias atuais sem exceção e às novas, em transação | | Salvar `enabled:false` | Pausa as instâncias que herdam; a mesma URL mantém o cursor | | Salvar a mesma URL sem `secret` | Preserva o segredo anterior | | Trocar URL | Começa após os eventos já existentes; informe o segredo do novo destino | | `POST /v1/webhook` com `{"action":"delete"}` | Remove o global e as inscrições herdadas; preserva as próprias | | `POST /webhook` com token de instância | Cria uma configuração própria, independente do global | | `POST /webhook` com `{"action":"delete"}` | Desativa essa instância inclusive em futuras mudanças globais | | `POST /webhook` com `{"action":"inherit"}` | Remove a exceção e passa a seguir o global, inclusive se cadastrado depois | A administração por ID usa `GET/POST /v1/instances/{id}/webhook` e `GET /v1/instances/{id}/webhook/errors`, com `admintoken`, sem reemitir o token. `GET /webhook` informa `inheritedGlobal` quando há inscrição. Uma instância sem destino pode optar por `inherit` antes de existir um global. Se não existir global, a ação remove o destino próprio e aguarda o futuro cadastro. Inscrições existentes antes desta versão são preservadas como configurações próprias. Para incluí-las no global, selecione **Usar webhook global** em cada uma. `ATENDROZAP_WEBHOOK_URL` e `ATENDROZAP_WEBHOOK_SECRET` inicializam o cadastro global uma única vez. A configuração salva no banco, inclusive sua remoção, prevalece sobre essas variáveis nos próximos reinícios. ## Identificar servidor, instância e empresa No Atendro, cadastre o servidor com URL base e token administrativo. Associe cada `instanceId` retornado por `/instance/init` ao servidor e à empresa autorizada. Use um segredo diferente por servidor e identifique o servidor pela rota/configuração do receptor, por exemplo `/webhooks/atendrozap/{serverId}`. Esse `serverId` é uma referência de cadastro do Atendro, não uma credencial. Não resolva permissões por `instanceName`, `owner`, nome de empresa ou número. Depois da assinatura, confira se o `instanceId` pertence àquele servidor e a uma empresa ativa. O envelope não contém um `serverId` autenticado nem `companyId`. Não compartilhe um segredo global entre empresas sem esse vínculo no receptor. ## Verificação do POST O servidor envia JSON com timeout de 10 segundos, sem seguir redirecionamentos. | Header | Conteúdo | |---|---| | `X-AtendroZAP-Event-Id` | UUID do evento; igual a `eventId` | | `X-AtendroZAP-Instance-Id` | UUID da instância; igual a `instanceId` | | `X-AtendroZAP-Event-Type` | Tipo projetado; igual a `EventType` | | `X-AtendroZAP-Event-Seq` | Sequência numérica; igual a `seq` | | `X-AtendroZAP-Attempt` | Tentativa; informativo, não faz parte da assinatura | | `X-AtendroZAP-Timestamp` | Unix em segundos, gerado a cada tentativa | | `X-AtendroZAP-Signature` | `sha256=` seguido de HMAC SHA-256 em hexadecimal | Assinatura: `HMAC-SHA256(segredo, bytes(timestamp + ".") + bytes(corpo_bruto))`. Verifique **antes** de chamar um parser que altere o corpo. Rejeite timestamps mais de 5 minutos no passado ou no futuro. A comparação deve usar uma primitiva criptográfica, como `crypto.subtle.verify`. Rejeite eventos sem assinatura. [verify-webhook.mjs](https://wpp.atendro.cloud/docs/examples/verify-webhook.mjs) implementa essa verificação, limita o corpo a 2 MiB e confere a correspondência dos headers com os campos assinados. É um módulo JavaScript sem dependências, importável também em uma Edge Function com Web Crypto. Os testes estão em `scripts/verify_webhook_test.mjs`. O módulo **não** grava eventos, resolve empresas ou retorna sucesso HTTP. O receptor do Atendro deve completar esse fluxo: 1. Resolver o servidor cadastrado a partir da rota e carregar seu segredo. 2. Chamar `verifyAtendroZAP(request, secret)` com o corpo ainda intacto. 3. Verificar o vínculo servidor → instância → empresa. 4. Gravar o envelope em uma inbox durável com restrição única em `(server_id, instance_id, event_id)`. Uma duplicata já gravada pode receber 2xx. 5. Responder 204/200 apenas depois do commit. Em falha temporária do banco, responder 503 para que o AtendroZAP tente novamente. 6. Processar a inbox com retry interno; marcar o processamento concluído na mesma transação das alterações do Atendro ou usando operações idempotentes. Se a plataforma exigir JWT por padrão na entrada, ajuste **somente essa função** para aceitar os headers HMAC deste contrato. O AtendroZAP não envia Bearer/JWT, apikey da plataforma nem headers customizados. A assinatura e o vínculo de instância são a autenticação e autorização do novo receptor. ## Eventos a tratar O envelope comum contém `eventId`, `seq`, `EventType`, `instanceId` e `instanceName`. `instance` pode ser string ou objeto conforme o evento; `owner` pode estar ausente. Use `instanceId` como identificador estável. | Tipo | Tratamento no Atendro | |---|---| | `connection` | Ler `state/status/connection` e atualizar conexão. `instance` é objeto. QR não é enviado: consultar status autenticado | | `messages` | Upsert de mensagem pelo ID externo; distinguir texto, mídia, contato, localização e `ReactionMessage` | | `messages_update` | Processar **todos** os `event.MessageIDs`; aplicar `Sent`, `Delivered`, `Read`, `Played` ou `Deleted`. `chat` pode ser string | | `messages_edit` | Atualizar o conteúdo do ID original; não criar nova mensagem | | `groups` | Ler `data` com `action`, `groupJid`, assunto e alterações de participantes | | `call` | Ler `data` com `callId`, `direction`, `status`, `endReason` e `endedBy`; emitido por instâncias `calls` | | `limits` | Ler `data` e apresentar a restrição da sessão | | `undecryptable` | Registrar estado de mensagem indecifrável sem inventar seu conteúdo | O formato detalhado e os exemplos estão em [EVENTS.md](https://wpp.atendro.cloud/docs/guias/eventos.md). Reações chegam como `messages` com `messageType: ReactionMessage`. Mensagens enviadas pela API e edições próprias têm projeções filtradas; o Atendro deve registrar a resposta do envio/edição e reconciliar por ID. Não dependa de um eco do webhook. `history`, `chats`, `qrcode`, `presence`, `contacts`, `labels`, `chat_labels`, `blocks`, `sender`, `newsletter_messages` são aceitos por compatibilidade, mas aparecem em `ignoredEvents`. Histórico é consultado por `/chat/find` e `/message/find`. Estados de ligações são entregues por `call` nas instâncias de tipo `calls`. ## Duplicação, ordem e recuperação A entrega é pelo menos uma vez. `seq` é crescente por instância, com lacunas possíveis; não é um contador exclusivo daquela instância. Eventos filtrados e recibos condensados não geram POST. Estados de entrega de mensagens não devem regredir quando ocorrer replay. Uma inbox deve processar todos os itens de um recibo em lote antes de marcar o evento concluído. 2xx confirma a entrega. `400/410/413/422` encerram imediatamente as tentativas; `401/403` encerram após 3. Rede, timeout e demais códigos usam retry com backoff, até 20 tentativas ou 48 horas. Após 10 falhas transitórias consecutivas há uma pausa de 5 minutos. A requisição já em trânsito pode concluir após uma mudança de configuração; a próxima entrega usa o cadastro atualizado. Consulte `GET /webhook` e `/webhook/errors`, ou as versões administrativas por ID. Com o token da instância, liste `GET /v1/events?status=dead` e use `POST /v1/events/{id}/replay` depois de corrigir o receptor. Se sua inbox já aceitou o evento, retente o processamento interno dela: reenviar o mesmo ID não deve duplicar a alteração. Trocar o destino não transfere eventos anteriores. ## Homologação do novo receptor Validar assinatura válida/inválida, janela de timestamp, instância de outro servidor/empresa, duplicatas concorrentes, falha de persistência, recibos com múltiplos IDs, mudança de conexão, reação, edição, mídia e recuperação após indisponibilidade. Não registrar payloads, números, conteúdo de conversas ou segredos em logs. Esta revisão testa o contrato e a entrega local com dados sintéticos; a homologação no Atendro real depende da implementação desse receptor. ## Registrar atendimento de ligação Assine `call` no webhook da instância de voz ou no webhook global herdado. O parceiro registra atendimento ao receber `EventType=call` com `data.event=answered`, `data.answered=true` e `data.answeredAt`. Nesse instante `data.status=connecting`; `active` indica mídia iniciada depois. Faça upsert por `(instanceId, data.callId)` e deduplique por `eventId`; o reenvio preserva esse ID. Os estados posteriores mantêm `answeredAt`. Subeventos `recording_ready`/`recording_failed` apenas atualizam a gravação. O download exige header `token` e deve ser intermediado pelo backend do Atendro. [Contrato completo de chamadas](https://wpp.atendro.cloud/docs/guias/chamadas-de-voz.md).