Envio de mensagens a grupos (API pública e dashboard)
Este documento descreve como a plataforma envia mensagens de template para grupos do WhatsApp, pela API pública e pela área logada (dashboard), e quais regras de negócio se aplicam. O processamento de fila, worker e webhooks segue o fluxo geral em api-publica-fluxo-envio-mensagem.md e message-analisys/fluxo-envio-mensagens.md.
Conceito: destino = grupo
Um grupo é identificado pelo JID do WhatsApp terminado em @g.us (ex.: [email protected]). Na API, esse valor vai no campo groupId, em substituição de destinationNumber (não é permitido enviar os dois no mesmo request).
A validação central está em sendMessageSchema (packages/shared/src/validation.ts):
groupId: string não vazia, deve casar com…@g.us.- Exatamente um de
destinationNumberougroupIdé obrigatório.
API pública
| Item | Valor |
|------|--------|
| Rota (Next.js App Router) | POST /api/v1/messages/send |
| Autenticação | Header x-api-key (ou x-api-key-id) — ver getPublicApiKeyInfo / public-api-auth |
| Handler compartilhado | handleSendWithKeyInfo em apps/fullstack/src/app/api/v1/messages/send/route.ts |
Exemplo mínimo de corpo (grupo):
{
"templateId": "seu-template",
"groupId": "[email protected]",
"variables": { "name": "Equipe" }
}
A documentação voltada ao cliente (incluindo groupId e demais campos opcionais) está em apps/fullstack/public/llms/docs-api/06-send-messages.md. A base URL pública publicada (ex.: https://pilotstatus.online/v1/...) pode ser exposta via gateway que remapeia o prefixo; o código do app vive em /api/v1/....
Resposta: 202 com id, correlationId, status, createdAt, origin (nome da instância WhatsApp), como nos envios para número individual.
Dashboard
| Item | Valor |
|------|--------|
| Rota interna | POST /api/internal/messages/send |
| Autenticação | Sessão do app (getAppSession) — usuário precisa estar logado no tenant |
| Arquivo | apps/fullstack/src/app/api/internal/messages/send/route.ts |
Fluxo resumido:
- Valida o JSON com
apiKeyId+templateId+ (destinationNumberougroupId) +variablesopcionais,deliverAt/deliverUntil,marketingOptionsopcional. - Carrega a API key do mesmo tenant que a sessão; se não achar,
404. - Chama
ensureEnvironmentAccess(permissão do usuário para o ambiente/projeto daquela chave). - Atualiza
lastUsedAtda chave. - Delega para o mesmo
handleSendWithKeyInfousado na API pública, passandosendContext:dashboardSession: trueeallowLiveMarketingOnPilotInstance: trueapenas se o e-mail do usuário for de administrador da plataforma (isAdminEmail).
Na UI da página de mensagens (apps/fullstack/src/spa/pages/Messages.tsx), o destino é um campo de texto. Se o valor (após trim) termina com @g.us, o front envia como groupId; caso contrário, como destinationNumber. Ou seja, o operador cola o JID do grupo no campo de destino, como faria com um telefone E.164.
Onde as duas entradas convergem
Tanto a API pública quanto o dashboard usam handleSendWithKeyInfo. A fila, gravação da mensagem e worker são as mesmas; a diferença principal entre origens está no contexto usado para a trava de template MARKETING na instância “Pilot Status” padrão (veja a tabela abaixo).
O destino efetivo enviado ao MessageService.send é o JID do grupo (campo interno destinationNumber no serviço guarda o destino, seja E.164 ou JID de grupo — ver comentário em message.service sobre “E.164 digits or group JID”).
Regras de negócio (resumo)
| Regra | Comportamento para grupos |
|--------|--------------------------------|
| Formato do id | groupId deve terminar em @g.us (validação Zod). |
| Ambiente TEST | Proibido enviar para grupo. Resposta 403 com GROUP_NOT_ALLOWED_IN_TEST. Em TEST, apenas números dos usuários do tenant (perfil) + número Pilot Status, quando aplicável, são permitidos para contato individual. |
| Ambiente LIVE e projeto | Exige productionApproved no projeto vinculado à API key, como para envio a contato (403 / PROJECT_NOT_APPROVED). |
| Template | Deve existir no mesmo projeto/ambiente da chave, com versão aprovada conforme as regras já usadas para envio 1:1. |
| LIVE + mídia no template | Se o corpo do template inclui mídia, aplica-se a regra de assinatura paga (402 / SUBSCRIPTION_REQUIRED_FOR_MEDIA), igual ao envio para número. |
| Instância WhatsApp | A API key deve resolver para uma instância existente (ou o default EVOLUTION_INSTANCE_NAME); mesmas mensagens de erro que para contato (ex. instância vinculada inexistente → 409 com LINKED_WHATSAPP_INSTANCE_NOT_FOUND). |
| Opt-in transacional | Para grupos, a checagem de opt-in transacional (WhatsAppTransactionalOptInService.assertDestinationAuthorized) não é aplicada — o destino de grupo entra no “bypass” de opt-in. A existência/validade do JID no WhatsApp é tratada no worker no momento do envio. |
| Template MARKETING + número Pilot Status (instância padrão) | Se o template é MARKETING e a instância usada é a padrão da plataforma (“Pilot Status”): o envio só é permitido se (a) for chamada com sessão de dashboard e o usuário for admin da plataforma, ou (b) a API key tiver sido criada por um usuário admin. Caso contrário, 403 com TEMPLATE_CATEGORY_MARKETING_REQUIRES_OWN_NUMBER. |
| Template OTP + número Pilot Status | Comportamento especial de bypass de opt-in para contatos não se aplica da mesma forma a grupos; para grupos já não há assert de opt-in. |
| Rate limit | Aplica-se ao destino; para grupo, o “número de destino” no limitador é o próprio JID (string do grupo). |
| Labels (opcional na API pública) | Se labels for enviado, o job assíncrono usa destinationType: "GROUP" e o destino é o groupId. Com retentionDays = 0 na chave, o vínculo pode não persistir (PII), como na documentação pública. |
| Agendamento | deliverAt e deliverUntil seguem a mesma semântica dos envios para contato. |
Códigos HTTP / code úteis (grupos)
400— validação Zod (ex.groupIdedestinationNumberjuntos, ou nenhum dos dois; formato degroupId).403—GROUP_NOT_ALLOWED_IN_TEST; opt-in (não aplica a grupo com bypass, mas aplica a contatos);TEMPLATE_CATEGORY_MARKETING_REQUIRES_OWN_NUMBER; outras regras de acesso.402— mídia em LIVE sem assinatura (SUBSCRIPTION_REQUIRED_FOR_MEDIA).404/409/429/500— mesmas famílias de erro do envio 1:1, conforme o caso.
Referências de código
| Tema | Onde |
|------|------|
| Schema compartilhado groupId / destinationNumber | packages/shared/src/validation.ts — sendMessageSchema |
| Lógica completa pós-parse | apps/fullstack/src/app/api/v1/messages/send/route.ts — handleSendWithKeyInfo |
| Rota interna (dashboard) | apps/fullstack/src/app/api/internal/messages/send/route.ts |
| Detecção @g.us no front | apps/fullstack/src/spa/pages/Messages.tsx — sendOne |
| Documentação de API pública (cliente) | apps/fullstack/public/llms/docs-api/06-send-messages.md |
Relacionados
- api-publica-fluxo-envio-mensagem.md — fluxo geral do endpoint público.
- webhooks.md e guias em
webhooks-*— correlação deidcom eventos, se o cliente rastreia o envio.