Pular para conteúdo

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:

5511999998888  ✓ (correto)
+5511999998888 ✗ (com +)
55 11 99999-8888 ✗ (com espaços/hífen)

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.