Integração: Evolution API¶
Visão Geral¶
A Evolution API é o gateway de comunicação WhatsApp utilizado pelo CIBA. Versão suportada: v2.3.7.
Responsabilidades: - Conexão e gerenciamento de sessões WhatsApp - Envio de mensagens (texto, mídia, reações, stickers) - Recebimento de webhooks de mensagens - Download de mídia recebida - Consulta de perfis de contato
Configuração¶
Variáveis de Ambiente¶
EVOLUTION_BASE_URL=http://localhost:8080
EVOLUTION_API_KEY=your-global-api-key
EVOLUTION_TIMEOUT_SECONDS=10
Options Pattern¶
// EvolutionOptions.cs
public class EvolutionOptions
{
public string BaseUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; } = 10;
}
// Registro em appsettings.json
{
"Evolution": {
"BaseUrl": "http://localhost:8080",
"ApiKey": "your-api-key",
"TimeoutSeconds": 10
}
}
Registro do Client¶
services.AddHttpClient<IWhatsAppClient, EvolutionApiClient>()
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
IWhatsAppClient — Abstração¶
A interface IWhatsAppClient abstrai a comunicação com Evolution API:
public interface IWhatsAppClient
{
// Mensagens
Task<SendMessageResult> SendTextMessageAsync(string instance, string phone, string text, CancellationToken ct);
Task<SendMessageResult> SendMediaMessageAsync(string instance, string phone, string mediaBase64,
string mediaType, string mimeType, string? fileName, string? caption, bool isVoiceMessage, CancellationToken ct);
Task<SendMessageResult> SendReactionAsync(string instance, string messageId, string remoteJid, string emoji, CancellationToken ct);
// Instância
Task<InstanceResult> CreateInstanceAsync(string instanceName, CancellationToken ct);
Task DeleteInstanceAsync(string instance, CancellationToken ct);
Task RestartInstanceAsync(string instance, CancellationToken ct);
Task LogoutAsync(string instance, CancellationToken ct);
// Conexão
Task<ConnectionInfo> GetConnectionStatusAsync(string instance, CancellationToken ct);
Task<QrCodeResult> GetQrCodeAsync(string instance, CancellationToken ct);
// Mídia e Perfil
Task<MediaDownloadResult?> DownloadMediaAsync(string instance, string messageId, string remoteJid, CancellationToken ct);
Task<ProfilePictureResult?> GetProfilePictureAsync(string instance, string phone, CancellationToken ct);
Task<ContactProfileResult?> GetContactProfileAsync(string instance, string phone, CancellationToken ct);
}
Endpoints Utilizados¶
Gerenciamento de Instância¶
| Método | Endpoint | Descrição |
|---|---|---|
| POST | /instance/create |
Cria nova instância |
| GET | /instance/connectionState/{instance} |
Status de conexão |
| GET | /instance/connect/{instance} |
Obtém QR code |
| PUT | /instance/restart/{instance} |
Reinicia instância |
| DELETE | /instance/logout/{instance} |
Desconecta WhatsApp |
| DELETE | /instance/delete/{instance} |
Remove instância |
Envio de Mensagens¶
| Método | Endpoint | Descrição |
|---|---|---|
| POST | /message/sendText/{instance} |
Envia texto |
| POST | /message/sendMedia/{instance} |
Envia imagem/vídeo/documento |
| POST | /message/sendWhatsAppAudio/{instance} |
Envia áudio PTT (voz) |
| POST | /message/sendReaction/{instance} |
Envia reação (emoji) |
Mídia e Perfil¶
| Método | Endpoint | Descrição |
|---|---|---|
| POST | /chat/getBase64FromMediaMessage/{instance} |
Download de mídia |
| POST | /chat/fetchProfilePictureUrl/{instance} |
URL da foto de perfil |
| POST | /chat/fetchProfile/{instance} |
Perfil completo do contato |
Envio de Mensagens¶
Texto¶
var result = await _whatsAppClient.SendTextMessageAsync(
instance: "minha-instancia",
phone: "5511999998888",
text: "Olá! Como posso ajudar?",
ct);
if (result.Success)
{
var whatsAppMessageId = result.WhatsAppMessageId;
}
Mídia (Imagem, Vídeo, Documento)¶
var result = await _whatsAppClient.SendMediaMessageAsync(
instance: "minha-instancia",
phone: "5511999998888",
mediaBase64: Convert.ToBase64String(imageBytes),
mediaType: "image", // image, video, document, audio
mimeType: "image/jpeg",
fileName: null, // Apenas para documents
caption: "Veja esta imagem",
isVoiceMessage: false,
ct);
Áudio (Voice Message)¶
// Áudio como PTT (Push-to-Talk) - aparece como mensagem de voz
var result = await _whatsAppClient.SendMediaMessageAsync(
instance: "minha-instancia",
phone: "5511999998888",
mediaBase64: audioBase64,
mediaType: "audio",
mimeType: "audio/ogg",
fileName: null,
caption: null,
isVoiceMessage: true, // true = PTT, false = arquivo de áudio
ct);
Reação¶
var result = await _whatsAppClient.SendReactionAsync(
instance: "minha-instancia",
messageId: "3EB0A8B1234567890",
remoteJid: "5511999998888@s.whatsapp.net",
emoji: "👍", // string vazia remove a reação
ct);
Estados de Conexão¶
var status = await _whatsAppClient.GetConnectionStatusAsync("minha-instancia", ct);
// status.State pode ser:
// - "open" → Conectado
// - "close" → Desconectado
// - "connecting" → Conectando
Mapeamento para ConnectionStatus (Domain):
| Evolution State | ConnectionStatus |
|---|---|
open |
Connected |
close |
Disconnected |
connecting |
Connecting |
| Erro na requisição | Failed |
Fluxo de Conexão¶
1. CreateInstanceAsync(name)
↓
2. GetQrCodeAsync(name) → QR code base64
↓
3. Usuário escaneia no WhatsApp
↓
4. Webhook CONNECTION_UPDATE → state: "open"
↓
5. Instância pronta para uso
QR Code Response:
public record QrCodeResult(
string? PairingCode, // Código de pareamento (alternativo ao QR)
string? Code, // Código bruto
string? Base64, // Imagem QR em base64
int Count // Número de tentativas
);
Download de Mídia¶
var media = await _whatsAppClient.DownloadMediaAsync(
instance: "minha-instancia",
messageId: "3EB0A8B1234567890",
remoteJid: "5511999998888@s.whatsapp.net",
ct);
if (media != null)
{
var bytes = Convert.FromBase64String(media.Base64);
var mimeType = media.MimeType; // "image/jpeg"
var fileName = media.FileName; // null ou nome original
}
Webhooks Recebidos¶
A Evolution API envia webhooks para eventos. O CIBA processa via endpoint /api/webhook/evolution.
Segurança do Webhook¶
O endpoint de webhook é protegido via Nginx Proxy Manager com restrição por IP interno.
Configuração no Nginx (produção):
# /api/webhook/evolution - SOMENTE interno
location /api/webhook/evolution {
allow 172.18.0.0/16;
allow 127.0.0.1;
deny all;
proxy_pass http://ciba-api:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# /api/* (resto) - liberado para todos
location /api {
proxy_pass http://ciba-api:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Tudo que não é /api - SOMENTE interno
location / {
allow 172.18.0.0/16;
allow 127.0.0.1;
deny all;
proxy_pass http://ciba-api:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Comportamento:
- /api/webhook/evolution → apenas rede interna (Evolution API)
- /api/* → público (protegido por JWT)
- / (health, swagger, etc.) → apenas rede interna
- Em desenvolvimento local, não há restrição
Eventos Principais¶
| Evento | Descrição |
|---|---|
MESSAGES_UPSERT |
Nova mensagem recebida |
MESSAGES_UPDATE |
Atualização de status (delivered, read) |
CONNECTION_UPDATE |
Mudança no status de conexão |
SEND_MESSAGE |
Confirmação de envio |
Estrutura do Webhook (MESSAGES_UPSERT)¶
{
"event": "messages.upsert",
"instance": "minha-instancia",
"data": {
"key": {
"remoteJid": "5511999998888@s.whatsapp.net",
"fromMe": false,
"id": "3EB0A8B1234567890"
},
"pushName": "João Silva",
"message": {
"conversation": "Olá, preciso de ajuda"
},
"messageTimestamp": 1705761600
}
}
Tipos de Mensagem no Payload¶
| Campo | Tipo |
|---|---|
message.conversation |
Texto simples |
message.extendedTextMessage |
Texto com preview/link |
message.imageMessage |
Imagem |
message.videoMessage |
Vídeo |
message.audioMessage |
Áudio |
message.documentMessage |
Documento |
message.stickerMessage |
Sticker |
message.reactionMessage |
Reação |
Consulta de Perfil¶
var profile = await _whatsAppClient.GetContactProfileAsync(
instance: "minha-instancia",
phone: "5511999998888",
ct);
if (profile != null)
{
var name = profile.Name; // Nome salvo ou pushName
var pictureUrl = profile.PictureUrl;
var exists = profile.NumberExists; // Se o número existe no WhatsApp
}
Nota: O pushName mais confiável vem do webhook de mensagens, não da API fetchProfile.
Tratamento de Erros¶
SendMessageResult¶
public record SendMessageResult(
bool Success,
string? WhatsAppMessageId,
string? Error
);
// Uso
if (!result.Success)
{
_logger.LogWarning("Falha ao enviar: {Error}", result.Error);
// Retry ou fallback
}
Retry Policy (Polly)¶
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
Formato de Telefone¶
O Evolution API espera telefones no formato internacional sem símbolos:
RemoteJid: Para operações que requerem JID, usar {phone}@s.whatsapp.net.
Health Check¶
public class EvolutionApiHealthCheck : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken ct)
{
// Verifica conectividade com Evolution API
// Não testa funcionalidade, apenas se está respondendo
}
}
Limitações Conhecidas¶
| Issue | Descrição | Workaround |
|---|---|---|
| #2004 | pushName vazio no fetchProfile |
Usar pushName do webhook |
| #896 | Filtro where não funciona em findContacts |
Usar fetchProfile |
| - | QR code expira em ~60 segundos | Implementar polling/refresh |
| - | Limite de mensagens por minuto | Implementar rate limiting no envio |
Ambiente de Desenvolvimento¶
Docker Compose para Evolution API:
evolution:
image: atendai/evolution-api:v2.3.7
environment:
- AUTHENTICATION_API_KEY=your-dev-key
- DATABASE_ENABLED=false
- CONFIG_SESSION_PHONE_CLIENT=Ciba Dev
ports:
- "8080:8080"
Importante: Em desenvolvimento, a instância WhatsApp conectada é a do próprio desenvolvedor.